Skip to content

Commit

Permalink
Add ability to set on_behalf_of for CBC elements (#8344)
Browse files Browse the repository at this point in the history
* Add ability to set on_behalf_of for CBC elements
  • Loading branch information
tjclawson-stripe committed May 15, 2024
1 parent 861f779 commit fc8f499
Show file tree
Hide file tree
Showing 13 changed files with 154 additions and 2 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
Dependencies updated in [8381](https://github.com/stripe/stripe-android/pull/8381):
* Bumped Play Services Wallet from 19.2.1 to 19.3.0.

### Payments
* [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 @@ -7408,9 +7408,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 @@ -7441,6 +7443,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 @@ -7459,6 +7462,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 @@ -7476,6 +7480,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 @@ -7495,6 +7500,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()
)

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()
}
}
}

0 comments on commit fc8f499

Please sign in to comment.