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

Add ability to set on_behalf_of for CBC elements #8344

Merged
merged 10 commits into from
May 15, 2024
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## XX.XX.XX - 2023-XX-XX

### Payments
jaynewstrom-stripe marked this conversation as resolved.
Show resolved Hide resolved
* [ADDED][8344](https://github.com/stripe/stripe-android/pull/8344) Added support for `onBehalfOf` to `CardInputWidget`, `CardMultilineWidget`, and `CardFormView`. This parameter may be required when setting a connected account as the merchant of record for a payment. For more information, see the [Connect docs](https://docs.stripe.com/connect/charges#on_behalf_of).

## 20.41.1 - 2024-04-22

### PaymentSheet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ abstract class AbsFakeStripeRepository : StripeRepository {

override suspend fun retrieveCardElementConfig(
requestOptions: ApiRequest.Options,
params: Map<String, String>?
): Result<MobileCardElementConfig> {
TODO("Not yet implemented")
}
Expand Down
6 changes: 6 additions & 0 deletions payments-core/api/payments-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -7412,9 +7412,11 @@ public final class com/stripe/android/view/CardFormView : android/widget/LinearL
public synthetic fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getBrand ()Lcom/stripe/android/model/CardBrand;
public final fun getCardParams ()Lcom/stripe/android/model/CardParams;
public final fun getOnBehalfOf ()Ljava/lang/String;
public final fun getPaymentMethodCreateParams ()Lcom/stripe/android/model/PaymentMethodCreateParams;
public final fun setCardValidCallback (Lcom/stripe/android/view/CardValidCallback;)V
public fun setEnabled (Z)V
public final fun setOnBehalfOf (Ljava/lang/String;)V
public final fun setPreferredNetworks (Ljava/util/List;)V
}

Expand Down Expand Up @@ -7445,6 +7447,7 @@ public final class com/stripe/android/view/CardInputWidget : android/widget/Line
public fun clear ()V
public final fun getBrand ()Lcom/stripe/android/model/CardBrand;
public fun getCardParams ()Lcom/stripe/android/model/CardParams;
public final fun getOnBehalfOf ()Ljava/lang/String;
public fun getPaymentMethodCard ()Lcom/stripe/android/model/PaymentMethodCreateParams$Card;
public fun getPaymentMethodCreateParams ()Lcom/stripe/android/model/PaymentMethodCreateParams;
public final fun getPostalCodeEnabled ()Z
Expand All @@ -7463,6 +7466,7 @@ public final class com/stripe/android/view/CardInputWidget : android/widget/Line
public fun setEnabled (Z)V
public fun setExpiryDate (II)V
public fun setExpiryDateTextWatcher (Landroid/text/TextWatcher;)V
public final fun setOnBehalfOf (Ljava/lang/String;)V
public final fun setPostalCodeEnabled (Z)V
public final fun setPostalCodeRequired (Z)V
public fun setPostalCodeTextWatcher (Landroid/text/TextWatcher;)V
Expand All @@ -7480,6 +7484,7 @@ public final class com/stripe/android/view/CardMultilineWidget : android/widget/
public fun clear ()V
public final synthetic fun getBrand ()Lcom/stripe/android/model/CardBrand;
public fun getCardParams ()Lcom/stripe/android/model/CardParams;
public final fun getOnBehalfOf ()Ljava/lang/String;
public final fun getPaymentMethodBillingDetails ()Lcom/stripe/android/model/PaymentMethod$BillingDetails;
public final fun getPaymentMethodBillingDetailsBuilder ()Lcom/stripe/android/model/PaymentMethod$BillingDetails$Builder;
public fun getPaymentMethodCard ()Lcom/stripe/android/model/PaymentMethodCreateParams$Card;
Expand All @@ -7499,6 +7504,7 @@ public final class com/stripe/android/view/CardMultilineWidget : android/widget/
public fun setEnabled (Z)V
public fun setExpiryDate (II)V
public fun setExpiryDateTextWatcher (Landroid/text/TextWatcher;)V
public final fun setOnBehalfOf (Ljava/lang/String;)V
public final fun setPostalCodeRequired (Z)V
public fun setPostalCodeTextWatcher (Landroid/text/TextWatcher;)V
public final fun setPreferredNetworks (Ljava/util/List;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1460,12 +1460,13 @@ class StripeApiRepository @JvmOverloads internal constructor(

override suspend fun retrieveCardElementConfig(
requestOptions: ApiRequest.Options,
params: Map<String, String>?
): Result<MobileCardElementConfig> {
return fetchStripeModelResult(
apiRequestFactory.createGet(
url = mobileCardElementConfigUrl,
options = requestOptions,
params = null,
params = params,
),
jsonParser = MobileCardElementConfigParser(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ interface StripeRepository {
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
suspend fun retrieveCardElementConfig(
requestOptions: ApiRequest.Options,
params: Map<String, String>? = null
): Result<MobileCardElementConfig>

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,23 @@ class CardFormView @JvmOverloads constructor(
val paymentMethodCreateParams: PaymentMethodCreateParams?
get() = paymentMethodCard?.let { PaymentMethodCreateParams.create(it) }

/**
* The Stripe account ID (if any) which is the business of record.
* See [use cases](https://docs.stripe.com/connect/charges#on_behalf_of) to determine if this option is relevant
* for your integration. This should match the
* [on_behalf_of](https://docs.stripe.com/api/payment_intents/create#create_payment_intent-on_behalf_of)
* provided on the Intent used when confirming payment.
*/
var onBehalfOf: String? = null
set(value) {
if (isAttachedToWindow) {
doWithCardWidgetViewModel(viewModelStoreOwner) { viewModel ->
viewModel.onBehalfOf = value
}
}
field = value
}

init {
orientation = VERTICAL

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,23 @@ class CardInputWidget @JvmOverloads constructor(
updatePostalRequired()
}

/**
* The Stripe account ID (if any) which is the business of record.
* See [use cases](https://docs.stripe.com/connect/charges#on_behalf_of) to determine if this option is relevant
* for your integration. This should match the
* [on_behalf_of](https://docs.stripe.com/api/payment_intents/create#create_payment_intent-on_behalf_of)
* provided on the Intent used when confirming payment.
*/
var onBehalfOf: String? = null
set(value) {
if (isAttachedToWindow) {
doWithCardWidgetViewModel(viewModelStoreOwner) { viewModel ->
viewModel.onBehalfOf = value
}
}
field = value
}

private fun updatePostalRequired() {
if (isPostalRequired()) {
requiredFields.add(postalCodeEditText)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,23 @@ class CardMultilineWidget @JvmOverloads constructor(
null
}

/**
* The Stripe account ID (if any) which is the business of record.
* See [use cases](https://docs.stripe.com/connect/charges#on_behalf_of) to determine if this option is relevant
* for your integration. This should match the
* [on_behalf_of](https://docs.stripe.com/api/payment_intents/create#create_payment_intent-on_behalf_of)
* provided on the Intent used when confirming payment.
*/
var onBehalfOf: String? = null
set(value) {
if (isAttachedToWindow) {
doWithCardWidgetViewModel(viewModelStoreOwner) { viewModel ->
viewModel.onBehalfOf = value
}
}
field = value
}

/**
* A [CardParams] representing the card details and postal code if all fields are valid;
* otherwise `null`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package com.stripe.android.view
import android.view.View
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.findViewTreeViewModelStoreOwner
import androidx.lifecycle.lifecycleScope
Expand All @@ -29,13 +31,24 @@ import javax.inject.Provider
internal class CardWidgetViewModel(
private val paymentConfigProvider: Provider<PaymentConfiguration>,
private val stripeRepository: StripeRepository,
dispatcher: CoroutineDispatcher = Dispatchers.IO,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
private val handle: SavedStateHandle
) : ViewModel() {

private val _isCbcEligible = MutableStateFlow(false)
val isCbcEligible: StateFlow<Boolean> = _isCbcEligible
var onBehalfOf: String? = handle[ON_BEHALF_OF]
set(value) {
field = value
handle[ON_BEHALF_OF] = value
getEligibility()
}

init {
getEligibility()
}

private fun getEligibility() {
viewModelScope.launch(dispatcher) {
_isCbcEligible.value = determineCbcEligibility()
}
Expand All @@ -49,6 +62,9 @@ internal class CardWidgetViewModel(
apiKey = paymentConfig.publishableKey,
stripeAccount = paymentConfig.stripeAccountId,
),
params = onBehalfOf?.let {
mapOf("on_behalf_of" to it)
}
)

val config = response.getOrNull()
Expand All @@ -69,9 +85,14 @@ internal class CardWidgetViewModel(
return CardWidgetViewModel(
paymentConfigProvider = { PaymentConfiguration.getInstance(context) },
stripeRepository = stripeRepository,
handle = extras.createSavedStateHandle()
) as T
}
}

companion object {
internal const val ON_BEHALF_OF = "on_behalf_of"
}
}

internal fun View.doWithCardWidgetViewModel(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.utils

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import com.stripe.android.ApiKeyFixtures
Expand All @@ -24,6 +25,7 @@ internal object CardElementTestHelper {
stripeRepository = object : AbsFakeStripeRepository() {
override suspend fun retrieveCardElementConfig(
requestOptions: ApiRequest.Options,
params: Map<String, String>?
): Result<MobileCardElementConfig> {
return Result.success(
MobileCardElementConfig(
Expand All @@ -34,6 +36,7 @@ internal object CardElementTestHelper {
)
}
},
handle = SavedStateHandle()
)

val viewModelStore = ViewModelStore().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class FakeCardElementConfigRepository : AbsFakeStripeRepository() {

override suspend fun retrieveCardElementConfig(
requestOptions: ApiRequest.Options,
params: Map<String, String>?
): Result<MobileCardElementConfig> {
return channel.receive()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.stripe.android.view
import android.text.TextWatcher
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.test.core.app.ApplicationProvider
Expand Down Expand Up @@ -1072,6 +1073,7 @@ internal class CardNumberEditTextTest {
paymentConfigProvider = { PaymentConfiguration.getInstance(context) },
stripeRepository = repository,
dispatcher = dispatcher,
handle = SavedStateHandle()
)

val store = ViewModelStore().apply {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.view

import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import com.stripe.android.PaymentConfiguration
Expand All @@ -26,6 +27,7 @@ class CardWidgetViewModelTest {
paymentConfigProvider = { paymentConfig },
stripeRepository = stripeRepository,
dispatcher = testDispatcher,
handle = SavedStateHandle()
)

viewModel.isCbcEligible.test {
Expand All @@ -44,6 +46,7 @@ class CardWidgetViewModelTest {
paymentConfigProvider = { paymentConfig },
stripeRepository = stripeRepository,
dispatcher = testDispatcher,
handle = SavedStateHandle()
)

viewModel.isCbcEligible.test {
Expand All @@ -61,6 +64,7 @@ class CardWidgetViewModelTest {
paymentConfigProvider = { paymentConfig },
stripeRepository = stripeRepository,
dispatcher = testDispatcher,
handle = SavedStateHandle()
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test to ensure it's saving to the handle?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍


viewModel.isCbcEligible.test {
Expand All @@ -69,4 +73,62 @@ class CardWidgetViewModelTest {
expectNoEvents()
}
}

@Test
fun `Saves OBO to savedStateHandle`() = runTest(testDispatcher) {
val stripeRepository = FakeCardElementConfigRepository()
val handle = SavedStateHandle()

val viewModel = CardWidgetViewModel(
paymentConfigProvider = { paymentConfig },
stripeRepository = stripeRepository,
dispatcher = testDispatcher,
handle = handle
)

viewModel.onBehalfOf = "test"
val obo: String? = handle["on_behalf_of"]
assertThat(obo).isEqualTo("test")
}

@Test
fun `Setting valid OBO re-fetches correct eligibility`() = runTest(testDispatcher) {
val stripeRepository = FakeCardElementConfigRepository()

val viewModel = CardWidgetViewModel(
paymentConfigProvider = { paymentConfig },
stripeRepository = stripeRepository,
dispatcher = testDispatcher,
handle = SavedStateHandle()
)

viewModel.isCbcEligible.test {
assertThat(awaitItem()).isFalse()
stripeRepository.enqueueEligible()
viewModel.onBehalfOf = "valid_obo"
assertThat(awaitItem()).isTrue()
}
}

@Test
fun `Setting invalid OBO re-fetches correct eligibility`() = runTest(testDispatcher) {
val stripeRepository = FakeCardElementConfigRepository()

val viewModel = CardWidgetViewModel(
paymentConfigProvider = { paymentConfig },
stripeRepository = stripeRepository,
dispatcher = testDispatcher,
handle = SavedStateHandle()
)

stripeRepository.enqueueEligible()

viewModel.isCbcEligible.test {
viewModel.onBehalfOf = "valid_obo"
assertThat(awaitItem()).isTrue()
stripeRepository.enqueueNotEligible()
viewModel.onBehalfOf = "invalid_obo"
assertThat(awaitItem()).isFalse()
}
}
}