From 712358aa57742eee0c30c6e7991ca20947224541 Mon Sep 17 00:00:00 2001 From: tjclawson Date: Thu, 25 Jun 2026 21:26:28 +0000 Subject: [PATCH 1/2] Add StripeClient per-instance publishable key to EmbeddedPaymentElement.configure() - Add StripeClient class (publishableKey + optional stripeAccountId, Parcelable) - Add StripeClientHolder @Singleton to carry per-configure() StripeClient - Add StripeClientModule replacing PaymentConfigurationModule in EmbeddedCommonModule; falls back to PaymentConfiguration singleton when no StripeClient is set - Add optional stripeClient param to both EmbeddedPaymentElement.configure() overloads; writes to holder before delegating to coordinator - Add TODO comment in EmbeddedConfigurationHandler noting that the cache key does not yet incorporate stripeClient - Add StripeClientTest covering key validation and field defaults Committed-By-Agent: goose --- .../java/com/stripe/android/StripeClient.kt | 17 +++++++ .../core/injection/StripeClientHolder.kt | 11 +++++ .../core/injection/StripeClientModule.kt | 30 ++++++++++++ .../com/stripe/android/StripeClientTest.kt | 46 +++++++++++++++++++ .../paymentelement/EmbeddedPaymentElement.kt | 7 +++ .../embedded/EmbeddedCommonModule.kt | 4 +- .../content/EmbeddedConfigurationHandler.kt | 1 + 7 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 payments-core/src/main/java/com/stripe/android/StripeClient.kt create mode 100644 payments-core/src/main/java/com/stripe/android/payments/core/injection/StripeClientHolder.kt create mode 100644 payments-core/src/main/java/com/stripe/android/payments/core/injection/StripeClientModule.kt create mode 100644 payments-core/src/test/java/com/stripe/android/StripeClientTest.kt diff --git a/payments-core/src/main/java/com/stripe/android/StripeClient.kt b/payments-core/src/main/java/com/stripe/android/StripeClient.kt new file mode 100644 index 00000000000..35337f9bf14 --- /dev/null +++ b/payments-core/src/main/java/com/stripe/android/StripeClient.kt @@ -0,0 +1,17 @@ +package com.stripe.android + +import android.os.Parcelable +import com.stripe.android.core.ApiKeyValidator +import dev.drewhamilton.poko.Poko +import kotlinx.parcelize.Parcelize + +@Parcelize +@Poko +class StripeClient( + val publishableKey: String, + val stripeAccountId: String? = null, +) : Parcelable { + init { + ApiKeyValidator.get().requireValid(publishableKey) + } +} diff --git a/payments-core/src/main/java/com/stripe/android/payments/core/injection/StripeClientHolder.kt b/payments-core/src/main/java/com/stripe/android/payments/core/injection/StripeClientHolder.kt new file mode 100644 index 00000000000..4aea6f0aef8 --- /dev/null +++ b/payments-core/src/main/java/com/stripe/android/payments/core/injection/StripeClientHolder.kt @@ -0,0 +1,11 @@ +package com.stripe.android.payments.core.injection + +import com.stripe.android.StripeClient +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class StripeClientHolder @Inject constructor() { + @Volatile + var stripeClient: StripeClient? = null +} diff --git a/payments-core/src/main/java/com/stripe/android/payments/core/injection/StripeClientModule.kt b/payments-core/src/main/java/com/stripe/android/payments/core/injection/StripeClientModule.kt new file mode 100644 index 00000000000..ef8f0d542f3 --- /dev/null +++ b/payments-core/src/main/java/com/stripe/android/payments/core/injection/StripeClientModule.kt @@ -0,0 +1,30 @@ +package com.stripe.android.payments.core.injection + +import android.content.Context +import com.stripe.android.PaymentConfiguration +import com.stripe.android.core.injection.PUBLISHABLE_KEY +import com.stripe.android.core.injection.STRIPE_ACCOUNT_ID +import dagger.Module +import dagger.Provides +import javax.inject.Named +import javax.inject.Provider + +@Module +class StripeClientModule { + @Provides + fun providePaymentConfiguration(context: Context, holder: StripeClientHolder): PaymentConfiguration { + return holder.stripeClient?.let { + PaymentConfiguration(it.publishableKey, it.stripeAccountId) + } ?: PaymentConfiguration.getInstance(context) + } + + @Provides + @Named(PUBLISHABLE_KEY) + fun providePublishableKey(paymentConfiguration: Provider): () -> String = + { paymentConfiguration.get().publishableKey } + + @Provides + @Named(STRIPE_ACCOUNT_ID) + fun provideStripeAccountId(paymentConfiguration: Provider): () -> String? = + { paymentConfiguration.get().stripeAccountId } +} diff --git a/payments-core/src/test/java/com/stripe/android/StripeClientTest.kt b/payments-core/src/test/java/com/stripe/android/StripeClientTest.kt new file mode 100644 index 00000000000..256ae5adf1d --- /dev/null +++ b/payments-core/src/test/java/com/stripe/android/StripeClientTest.kt @@ -0,0 +1,46 @@ +package com.stripe.android + +import com.google.common.truth.Truth.assertThat +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.Test +import kotlin.test.assertFailsWith + +@RunWith(RobolectricTestRunner::class) +class StripeClientTest { + + @Test + fun `valid pk_test_ key is accepted`() { + val client = StripeClient(publishableKey = ApiKeyFixtures.FAKE_PUBLISHABLE_KEY) + assertThat(client.publishableKey).isEqualTo(ApiKeyFixtures.FAKE_PUBLISHABLE_KEY) + } + + @Test + fun `key starting with sk_ throws IllegalArgumentException`() { + assertFailsWith { + StripeClient(publishableKey = "sk_test_123") + } + } + + @Test + fun `blank key throws`() { + assertFailsWith { + StripeClient(publishableKey = "") + } + } + + @Test + fun `stripeAccountId is null by default`() { + val client = StripeClient(publishableKey = ApiKeyFixtures.FAKE_PUBLISHABLE_KEY) + assertThat(client.stripeAccountId).isNull() + } + + @Test + fun `stripeAccountId is set correctly when provided`() { + val client = StripeClient( + publishableKey = ApiKeyFixtures.FAKE_PUBLISHABLE_KEY, + stripeAccountId = "acct_test_123", + ) + assertThat(client.stripeAccountId).isEqualTo("acct_test_123") + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt index aecf725d253..d148fdafabd 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt @@ -16,6 +16,8 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner import com.stripe.android.ExperimentalAllowsRemovalOfLastSavedPaymentMethodApi import com.stripe.android.SharedPaymentTokenSessionPreview +import com.stripe.android.StripeClient +import com.stripe.android.payments.core.injection.StripeClientHolder import com.stripe.android.checkout.Checkout import com.stripe.android.checkout.CheckoutConfigurationMerger import com.stripe.android.checkout.CheckoutInstances @@ -59,6 +61,7 @@ class EmbeddedPaymentElement @Inject internal constructor( paymentOptionDisplayDataHolder: PaymentOptionDisplayDataHolder, private val configurationCoordinator: EmbeddedConfigurationCoordinator, stateHelper: EmbeddedStateHelper, + private val stripeClientHolder: StripeClientHolder, ) { /** @@ -84,7 +87,9 @@ class EmbeddedPaymentElement @Inject internal constructor( suspend fun configure( intentConfiguration: PaymentSheet.IntentConfiguration, configuration: Configuration, + stripeClient: StripeClient? = null, ): ConfigureResult { + stripeClientHolder.stripeClient = stripeClient val initializationMode = PaymentElementLoader.InitializationMode.DeferredIntent(intentConfiguration) return configurationCoordinator.configure(configuration, initializationMode) } @@ -101,7 +106,9 @@ class EmbeddedPaymentElement @Inject internal constructor( suspend fun configure( checkout: Checkout, configuration: Configuration, + stripeClient: StripeClient? = null, ): ConfigureResult { + stripeClientHolder.stripeClient = stripeClient CheckoutInstances.ensureNoMutationInFlight(checkout.internalState.key) return configurationCoordinator.configure( configuration = CheckoutConfigurationMerger.EmbeddedConfiguration(configuration) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedCommonModule.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedCommonModule.kt index c4396ab38d3..215e88fdc21 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedCommonModule.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedCommonModule.kt @@ -23,7 +23,7 @@ import com.stripe.android.paymentelement.confirmation.ALLOWS_MANUAL_CONFIRMATION import com.stripe.android.payments.core.analytics.ErrorReporter import com.stripe.android.payments.core.analytics.RealErrorReporter import com.stripe.android.payments.core.injection.PRODUCT_USAGE -import com.stripe.android.payments.core.injection.PaymentConfigurationModule +import com.stripe.android.payments.core.injection.StripeClientModule import com.stripe.android.payments.core.injection.StripeRepositoryModule import com.stripe.android.paymentsheet.BuildConfig import com.stripe.android.paymentsheet.CustomerStateHolder @@ -53,7 +53,7 @@ import kotlin.coroutines.CoroutineContext TapToAddConnectionModule::class, PaymentsIntegrityModule::class, PaymentElementRequestSurfaceModule::class, - PaymentConfigurationModule::class, + StripeClientModule::class, StripeNetworkClientModule::class, PaymentOptionCardArtModule::class, NfcScanningAvailabilityModule::class, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/content/EmbeddedConfigurationHandler.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/content/EmbeddedConfigurationHandler.kt index d8348c2baef..ae195033a0a 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/content/EmbeddedConfigurationHandler.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/content/EmbeddedConfigurationHandler.kt @@ -112,6 +112,7 @@ internal class DefaultEmbeddedConfigurationHandler @Inject constructor( return coalescingOrchestrator.get() } + // TODO: Include stripeClient in cache key once StripeClient is threaded through to Arguments @Parcelize data class Arguments( val initializationMode: PaymentElementLoader.InitializationMode, From 5762fdbcc72fc22dfdc8c1e84c63332d29d6d92e Mon Sep 17 00:00:00 2001 From: Tyler Clawson Date: Tue, 30 Jun 2026 17:20:53 -0400 Subject: [PATCH 2/2] Update --- .../playground/embedded/EmbeddedPlaygroundActivity.kt | 1 + .../example/playground/settings/PlaygroundSettings.kt | 2 ++ .../android/paymentelement/EmbeddedPaymentElement.kt | 8 ++++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/embedded/EmbeddedPlaygroundActivity.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/embedded/EmbeddedPlaygroundActivity.kt index bc8758089cb..d1c0b372ffe 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/embedded/EmbeddedPlaygroundActivity.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/embedded/EmbeddedPlaygroundActivity.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.stripe.android.StripeClient import com.stripe.android.checkout.Checkout import com.stripe.android.model.PaymentMethod import com.stripe.android.paymentelement.AnalyticEvent diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/PlaygroundSettings.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/PlaygroundSettings.kt index 23e226e844c..74058c761de 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/PlaygroundSettings.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/PlaygroundSettings.kt @@ -9,6 +9,7 @@ import android.util.Log import androidx.compose.runtime.Stable import androidx.core.content.edit import com.stripe.android.PaymentConfiguration +import com.stripe.android.StripeClient import com.stripe.android.checkout.Checkout import com.stripe.android.core.utils.FeatureFlags import com.stripe.android.customersheet.CustomerSheet @@ -179,6 +180,7 @@ internal class PlaygroundSettings private constructor( }.onEach { (settingDefinition, value) -> settingDefinition.configure(value, builder, playgroundState, embeddedConfigurationData) } + builder.stripeClient(StripeClient("pk_123")) return builder.build() } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt index d148fdafabd..58aa5664abf 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt @@ -87,9 +87,8 @@ class EmbeddedPaymentElement @Inject internal constructor( suspend fun configure( intentConfiguration: PaymentSheet.IntentConfiguration, configuration: Configuration, - stripeClient: StripeClient? = null, ): ConfigureResult { - stripeClientHolder.stripeClient = stripeClient + stripeClientHolder.stripeClient = configuration.stripeClient val initializationMode = PaymentElementLoader.InitializationMode.DeferredIntent(intentConfiguration) return configurationCoordinator.configure(configuration, initializationMode) } @@ -312,6 +311,7 @@ class EmbeddedPaymentElement @Inject internal constructor( internal val termsDisplay: Map = emptyMap(), internal val opensCardScannerAutomatically: Boolean = ConfigurationDefaults.opensCardScannerAutomatically, internal val userOverrideCountry: String? = ConfigurationDefaults.userOverrideCountry, + internal val stripeClient: StripeClient? = null ) : Parcelable { @Suppress("TooManyFunctions") class Builder( @@ -349,6 +349,9 @@ class EmbeddedPaymentElement @Inject internal constructor( private var opensCardScannerAutomatically: Boolean = ConfigurationDefaults.opensCardScannerAutomatically private var userOverrideCountry: String? = ConfigurationDefaults.userOverrideCountry + private var stripeClient: StripeClient? = null + + fun stripeClient(stripeClient: StripeClient?) = apply { this.stripeClient = stripeClient } /** * If set, the customer can select a previously saved payment method. @@ -609,6 +612,7 @@ class EmbeddedPaymentElement @Inject internal constructor( termsDisplay = termsDisplay, opensCardScannerAutomatically = opensCardScannerAutomatically, userOverrideCountry = userOverrideCountry, + stripeClient = stripeClient ) }