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

Allow instant_or_skip verification for ACH #9695

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# CHANGELOG

## XX.XX.XX - 20XX-XX-XX
### PaymentSheet
- [CHANGED][9695](https://github.com/stripe/stripe-android/pull/9695) US Bank Account now supports the `instant_or_skip` verification method.

## 21.2.0 - 2024-11-19
### Payments
Expand Down
24 changes: 24 additions & 0 deletions financial-connections/api/financial-connections.api
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,30 @@ public final class com/stripe/android/financialconnections/FinancialConnectionsS
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/financialconnections/FinancialConnectionsSheetInternalResult$Canceled$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheetInternalResult$Canceled;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/stripe/android/financialconnections/FinancialConnectionsSheetInternalResult$Canceled;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/financialconnections/FinancialConnectionsSheetInternalResult$Completed$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheetInternalResult$Completed;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/stripe/android/financialconnections/FinancialConnectionsSheetInternalResult$Completed;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/financialconnections/FinancialConnectionsSheetInternalResult$Failed$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheetInternalResult$Failed;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/stripe/android/financialconnections/FinancialConnectionsSheetInternalResult$Failed;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/financialconnections/FinancialConnectionsSheetRedirectActivity : androidx/appcompat/app/AppCompatActivity {
public static final field $stable I
public fun <init> ()V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ class FinancialConnectionsSheet internal constructor(
callback: FinancialConnectionsSheetResultCallback
): FinancialConnectionsSheet {
return FinancialConnectionsSheet(
FinancialConnectionsSheetForDataLauncher(activity, callback)
FinancialConnectionsSheetForDataLauncher(activity) {
callback.onFinancialConnectionsSheetResult(it.toPublicResult())
}
)
}

Expand All @@ -145,7 +147,9 @@ class FinancialConnectionsSheet internal constructor(
callback: FinancialConnectionsSheetResultCallback
): FinancialConnectionsSheet {
return FinancialConnectionsSheet(
FinancialConnectionsSheetForDataLauncher(fragment, callback)
FinancialConnectionsSheetForDataLauncher(fragment) {
callback.onFinancialConnectionsSheetResult(it.toPublicResult())
}
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fun rememberFinancialConnectionsSheet(
): FinancialConnectionsSheet {
val activityResultLauncher = rememberLauncherForActivityResult(
FinancialConnectionsSheetForDataContract()
) { callback(it) }
) { callback(it.toPublicResult()) }
return remember {
FinancialConnectionsSheet(
FinancialConnectionsSheetForDataLauncher(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.stripe.android.financialconnections

import android.os.Parcelable
import androidx.annotation.RestrictTo
import com.stripe.android.financialconnections.FinancialConnectionsSheetInternalResult.Canceled
import com.stripe.android.financialconnections.FinancialConnectionsSheetInternalResult.Completed
import com.stripe.android.financialconnections.FinancialConnectionsSheetInternalResult.Failed
import com.stripe.android.financialconnections.model.FinancialConnectionsSession
import kotlinx.parcelize.Parcelize

Expand All @@ -14,7 +18,7 @@ sealed class FinancialConnectionsSheetResult : Parcelable {
*/
@Parcelize
data class Completed(
val financialConnectionsSession: FinancialConnectionsSession
val financialConnectionsSession: FinancialConnectionsSession,
) : FinancialConnectionsSheetResult()

/**
Expand All @@ -32,3 +36,33 @@ sealed class FinancialConnectionsSheetResult : Parcelable {
val error: Throwable
) : FinancialConnectionsSheetResult()
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
sealed class FinancialConnectionsSheetInternalResult : Parcelable {

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Parcelize
data class Completed(
val financialConnectionsSession: FinancialConnectionsSession,
val manualEntryUsesMicrodeposits: Boolean,
) : FinancialConnectionsSheetInternalResult()

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Parcelize
data object Canceled : FinancialConnectionsSheetInternalResult()

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Parcelize
data class Failed(
val error: Throwable
) : FinancialConnectionsSheetInternalResult()
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun FinancialConnectionsSheetInternalResult.toPublicResult(): FinancialConnectionsSheetResult {
return when (this) {
is Canceled -> FinancialConnectionsSheetResult.Canceled
is Failed -> FinancialConnectionsSheetResult.Failed(error)
is Completed -> FinancialConnectionsSheetResult.Completed(financialConnectionsSession)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package com.stripe.android.financialconnections

import androidx.annotation.RestrictTo

/**
* Callback that is invoked when a [FinancialConnectionsSheetResult] is available.
*/
fun interface FinancialConnectionsSheetResultCallback {
fun onFinancialConnectionsSheetResult(financialConnectionsSheetResult: FinancialConnectionsSheetResult)
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun interface FinancialConnectionsSheetInternalResultCallback {
fun onFinancialConnectionsSheetResult(financialConnectionsSheetResult: FinancialConnectionsSheetInternalResult)
}
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,10 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
}.onSuccess {
finishWithResult(
state = state,
result = Completed(financialConnectionsSession = it)
result = Completed(
financialConnectionsSession = it,
manualEntryUsesMicrodeposits = state.manifest?.manualEntryUsesMicrodeposits ?: false,
)
)
}.onFailure { error ->
finishWithResult(stateFlow.value, Failed(error))
Expand All @@ -322,7 +325,11 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
}.onSuccess { (las, token) ->
finishWithResult(
state = state,
result = Completed(financialConnectionsSession = las, token = token)
result = Completed(
financialConnectionsSession = las,
token = token,
manualEntryUsesMicrodeposits = state.manifest?.manualEntryUsesMicrodeposits ?: false,
)
)
}.onFailure { error ->
finishWithResult(stateFlow.value, Failed(error))
Expand Down Expand Up @@ -458,7 +465,8 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor(
bankName = url.getQueryParameter(QUERY_BANK_NAME)
),
financialConnectionsSession = null,
token = null
token = null,
manualEntryUsesMicrodeposits = false,
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ internal sealed class FinancialConnectionsSheetActivityResult : Parcelable {
* @param financialConnectionsSession The financial connections session connected
*/
@Parcelize
data class Completed(
data class Completed constructor(
// Instant Debits sessions: return payment method id and bank details.
val instantDebits: InstantDebitsResult? = null,
// non-Link sessions: return full LinkedAccountSession
val financialConnectionsSession: FinancialConnectionsSession? = null,
// Bank account Token sessions: session + token.
val token: Token? = null
val token: Token? = null,
// Temporary field to expose to callers whether the session used microdeposits.
val manualEntryUsesMicrodeposits: Boolean,
) : FinancialConnectionsSheetActivityResult()

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
import com.stripe.android.financialconnections.FinancialConnectionsSheetActivity
import com.stripe.android.financialconnections.FinancialConnectionsSheetResult
import com.stripe.android.financialconnections.FinancialConnectionsSheetInternalResult
import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Companion.EXTRA_RESULT

internal class FinancialConnectionsSheetForDataContract :
ActivityResultContract<FinancialConnectionsSheetActivityArgs.ForData, FinancialConnectionsSheetResult>() {
ActivityResultContract<FinancialConnectionsSheetActivityArgs.ForData, FinancialConnectionsSheetInternalResult>() {

override fun createIntent(
context: Context,
Expand All @@ -23,30 +23,31 @@ internal class FinancialConnectionsSheetForDataContract :
override fun parseResult(
resultCode: Int,
intent: Intent?
): FinancialConnectionsSheetResult {
): FinancialConnectionsSheetInternalResult {
return intent
?.getParcelableExtra<FinancialConnectionsSheetActivityResult>(EXTRA_RESULT)
?.toExposedResult()
?: FinancialConnectionsSheetResult.Failed(
?.toResult()
?: FinancialConnectionsSheetInternalResult.Failed(
IllegalArgumentException("Failed to retrieve a ConnectionsSheetResult.")
)
}

private fun FinancialConnectionsSheetActivityResult.toExposedResult(): FinancialConnectionsSheetResult =
private fun FinancialConnectionsSheetActivityResult.toResult(): FinancialConnectionsSheetInternalResult =
when (this) {
is FinancialConnectionsSheetActivityResult.Canceled -> FinancialConnectionsSheetResult.Canceled
is FinancialConnectionsSheetActivityResult.Failed -> FinancialConnectionsSheetResult.Failed(
is FinancialConnectionsSheetActivityResult.Canceled -> FinancialConnectionsSheetInternalResult.Canceled
is FinancialConnectionsSheetActivityResult.Failed -> FinancialConnectionsSheetInternalResult.Failed(
error
)

is FinancialConnectionsSheetActivityResult.Completed ->
when (financialConnectionsSession) {
null -> FinancialConnectionsSheetResult.Failed(
null -> FinancialConnectionsSheetInternalResult.Failed(
IllegalArgumentException("financialConnectionsSession not set.")
)

else -> FinancialConnectionsSheetResult.Completed(
financialConnectionsSession = financialConnectionsSession
else -> FinancialConnectionsSheetInternalResult.Completed(
financialConnectionsSession = financialConnectionsSession,
manualEntryUsesMicrodeposits = manualEntryUsesMicrodeposits,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import androidx.activity.result.ActivityResultRegistry
import androidx.annotation.RestrictTo
import androidx.fragment.app.Fragment
import com.stripe.android.financialconnections.FinancialConnectionsSheet
import com.stripe.android.financialconnections.FinancialConnectionsSheetResultCallback
import com.stripe.android.financialconnections.FinancialConnectionsSheetInternalResultCallback
import org.jetbrains.annotations.TestOnly

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Expand All @@ -16,7 +16,7 @@ class FinancialConnectionsSheetForDataLauncher(

constructor(
activity: ComponentActivity,
callback: FinancialConnectionsSheetResultCallback
callback: FinancialConnectionsSheetInternalResultCallback
) : this(
activity.registerForActivityResult(
FinancialConnectionsSheetForDataContract()
Expand All @@ -27,7 +27,7 @@ class FinancialConnectionsSheetForDataLauncher(

constructor(
fragment: Fragment,
callback: FinancialConnectionsSheetResultCallback
callback: FinancialConnectionsSheetInternalResultCallback
) : this(
fragment.registerForActivityResult(
FinancialConnectionsSheetForDataContract()
Expand All @@ -40,7 +40,7 @@ class FinancialConnectionsSheetForDataLauncher(
constructor(
fragment: Fragment,
registry: ActivityResultRegistry,
callback: FinancialConnectionsSheetResultCallback
callback: FinancialConnectionsSheetInternalResultCallback
) : this(
fragment.registerForActivityResult(
FinancialConnectionsSheetForDataContract(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ internal class FinancialConnectionsSheetNativeViewModel @Inject constructor(
if (state.isLinkWithStripe) {
handleInstantDebitsCompletion(session)
} else {
handleFinancialConnectionsCompletion(session)
handleFinancialConnectionsCompletion(session, state.manualEntryUsesMicrodeposits)
}
}

Expand Down Expand Up @@ -364,7 +364,10 @@ internal class FinancialConnectionsSheetNativeViewModel @Inject constructor(
}
}

private fun handleFinancialConnectionsCompletion(session: FinancialConnectionsSession) {
private fun handleFinancialConnectionsCompletion(
session: FinancialConnectionsSession,
manualEntryUsesMicrodeposits: Boolean,
) {
FinancialConnections.emitEvent(
name = Name.SUCCESS,
metadata = Metadata(
Expand All @@ -374,7 +377,8 @@ internal class FinancialConnectionsSheetNativeViewModel @Inject constructor(
finishWithResult(
Completed(
financialConnectionsSession = session,
token = session.parsedToken
token = session.parsedToken,
manualEntryUsesMicrodeposits = manualEntryUsesMicrodeposits,
)
)
}
Expand All @@ -385,7 +389,10 @@ internal class FinancialConnectionsSheetNativeViewModel @Inject constructor(
}

val result = if (instantDebits != null) {
Completed(instantDebits = instantDebits)
Completed(
instantDebits = instantDebits,
manualEntryUsesMicrodeposits = false,
)
} else {
Failed(
error = UnclassifiedError(
Expand Down Expand Up @@ -514,6 +521,7 @@ internal data class FinancialConnectionsSheetNativeState(
val initialPane: Pane,
val theme: Theme,
val isLinkWithStripe: Boolean,
val manualEntryUsesMicrodeposits: Boolean,
val elementsSessionContext: ElementsSessionContext?,
) {

Expand All @@ -535,6 +543,7 @@ internal data class FinancialConnectionsSheetNativeState(
theme = args.initialSyncResponse.manifest.theme?.toLocalTheme() ?: Theme.default,
viewEffect = null,
isLinkWithStripe = args.initialSyncResponse.manifest.isLinkWithStripe ?: false,
manualEntryUsesMicrodeposits = args.initialSyncResponse.manifest.manualEntryUsesMicrodeposits,
elementsSessionContext = args.elementsSessionContext,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class FinancialConnectionsSheetForDataContractTest {
@Test
fun `parseResult() with missing data should return failed result`() {
assertThat(FinancialConnectionsSheetForDataContract().parseResult(0, Intent()))
.isInstanceOf(FinancialConnectionsSheetResult.Failed::class.java)
.isInstanceOf(FinancialConnectionsSheetInternalResult.Failed::class.java)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,10 @@ class FinancialConnectionsSheetViewModelTest {
assertThat(it.webAuthFlowStatus).isEqualTo(AuthFlowStatus.NONE)
val viewEffect = it.viewEffect as FinishWithResult
assertThat(viewEffect.result).isEqualTo(
Completed(financialConnectionsSession = expectedSession)
Completed(
financialConnectionsSession = expectedSession,
manualEntryUsesMicrodeposits = true,
)
)
}
}
Expand Down Expand Up @@ -658,6 +661,29 @@ class FinancialConnectionsSheetViewModelTest {
}
}

@Test
fun `Returns correct result when manual entry does not use microdeposits`() {
runTest {
// Given
val viewModel = createViewModel(
defaultInitialState.copy(
manifest = syncResponse.manifest.copy(manualEntryUsesMicrodeposits = false),
webAuthFlowStatus = AuthFlowStatus.ON_EXTERNAL_ACTIVITY,
)
)

// When
// simulate success
viewModel.handleOnNewIntent(successIntent())

withState(viewModel) {
val viewEffect = it.viewEffect as FinishWithResult
val success = viewEffect.result as Completed
assertThat(success.manualEntryUsesMicrodeposits).isFalse()
}
}
}

@Test
fun `init - when repository returns sync response, stores in state`() {
runTest {
Expand Down
Loading
Loading