From bddf05b9366c2898274a1a58e4e2ee4b01e535ad Mon Sep 17 00:00:00 2001 From: Chengtin Tsai Date: Fri, 16 Oct 2026 09:56:18 -0700 Subject: [PATCH 1/2] Share Address Element appearance across screens Committed-By-Agent: codex Co-authored-by: codex --- .../addresselement/AddressElementActivity.kt | 5 +- .../addresselement/AutocompleteScreen.kt | 4 +- .../addresselement/InputAddressScreen.kt | 108 +++++++++--------- .../AddressElementActivityTest.kt | 81 +++++++++++++ .../InputAddressScreenScreenshotTest.kt | 97 ++++++++-------- 5 files changed, 187 insertions(+), 108 deletions(-) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivity.kt index a4f9b7f475f..f15d419b036 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivity.kt @@ -23,9 +23,10 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.stripe.android.common.ui.ElementsBottomSheetLayout +import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.addresselement.AddressElementResultStateHolder.State import com.stripe.android.paymentsheet.parseAppearance -import com.stripe.android.uicore.StripeTheme +import com.stripe.android.paymentsheet.ui.PaymentElementTheme import com.stripe.android.uicore.elements.bottomsheet.StripeBottomSheetState import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetState import com.stripe.android.uicore.utils.fadeOut @@ -96,7 +97,7 @@ internal class AddressElementActivity : ComponentActivity() { bottomSheetState: StripeBottomSheetState, navController: NavHostController, ) { - StripeTheme { + PaymentElementTheme(appearance = starterArgs?.config?.appearance ?: PaymentSheet.Appearance()) { ElementsBottomSheetLayout( state = bottomSheetState, onDismissed = viewModel.resultStateHolder::onUserCancel, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreen.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreen.kt index b56b105e5f0..4b20c0400aa 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreen.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/AutocompleteScreen.kt @@ -6,7 +6,6 @@ import androidx.annotation.VisibleForTesting import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -47,6 +46,7 @@ import com.stripe.android.uicore.getOuterFormInsets import com.stripe.android.uicore.shouldUseDarkDynamicColor import com.stripe.android.uicore.stripeColors import com.stripe.android.uicore.stripeFormInsets +import com.stripe.android.uicore.stripeThemeIsDark import com.stripe.android.uicore.text.annotatedStringResource import com.stripe.android.uicore.utils.collectAsState import kotlinx.coroutines.flow.collectLatest @@ -79,7 +79,7 @@ internal fun AutocompleteScreenUI( viewModel: AutocompleteViewModel, navigator: AddressElementNavigator, attributionDrawable: Int? = - PlacesClientProxy.getPlacesPoweredByGoogleDrawable(isSystemInDarkTheme()) + PlacesClientProxy.getPlacesPoweredByGoogleDrawable(MaterialTheme.stripeThemeIsDark) ) { LaunchedEffect(Unit) { viewModel.event.collectLatest { event -> diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressScreen.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressScreen.kt index ae8786bee9d..03728b659cb 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressScreen.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressScreen.kt @@ -20,12 +20,10 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.stripe.android.common.ui.PrimaryButton import com.stripe.android.core.strings.ResolvableString import com.stripe.android.core.strings.resolvableString -import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.R import com.stripe.android.paymentsheet.injection.InputAddressViewModelSubcomponent import com.stripe.android.paymentsheet.ui.AddressOptionsAppBar import com.stripe.android.paymentsheet.ui.ErrorMessage -import com.stripe.android.paymentsheet.ui.PaymentElementTheme import com.stripe.android.ui.core.FormUI import com.stripe.android.uicore.elements.CheckboxElementUI import com.stripe.android.uicore.getOuterFormInsets @@ -37,7 +35,6 @@ import javax.inject.Provider @Composable internal fun InputAddressScreen( - appearance: PaymentSheet.Appearance, primaryButtonEnabled: Boolean, primaryButtonLoading: Boolean, closeButtonEnabled: Boolean, @@ -52,63 +49,61 @@ internal fun InputAddressScreen( saveError: ResolvableString?, ) { val focusManager = LocalFocusManager.current - PaymentElementTheme(appearance = appearance) { - Scaffold( - modifier = Modifier - .fillMaxHeight() - .imePadding(), - backgroundColor = MaterialTheme.colors.surface, - contentColor = MaterialTheme.colors.onSurface, - topBar = { - AddressOptionsAppBar( - isRootScreen = true, - isEnabled = closeButtonEnabled, - onButtonClick = { - focusManager.clearFocus() - onCloseClick() - } - ) - } + Scaffold( + modifier = Modifier + .fillMaxHeight() + .imePadding(), + backgroundColor = MaterialTheme.colors.surface, + contentColor = MaterialTheme.colors.onSurface, + topBar = { + AddressOptionsAppBar( + isRootScreen = true, + isEnabled = closeButtonEnabled, + onButtonClick = { + focusManager.clearFocus() + onCloseClick() + } + ) + } + ) { + ScrollableColumn( + modifier = Modifier.padding(it) ) { - ScrollableColumn( - modifier = Modifier.padding(it) + Column( + Modifier + .padding(MaterialTheme.stripeFormInsets.getOuterFormInsets()) + .padding(top = MaterialTheme.stripeFormInsets.top.dp) + .padding(bottom = MaterialTheme.stripeFormInsets.bottom.dp) ) { - Column( - Modifier - .padding(MaterialTheme.stripeFormInsets.getOuterFormInsets()) - .padding(top = MaterialTheme.stripeFormInsets.top.dp) - .padding(bottom = MaterialTheme.stripeFormInsets.bottom.dp) - ) { - Text( - title, - style = MaterialTheme.typography.h4, - modifier = Modifier.padding(bottom = 8.dp) - ) - topContent() - formContent() - bottomContent() - saveError?.let { - ErrorMessage( - error = it.resolve(), - modifier = Modifier.padding(top = 2.dp), - ) - } - PrimaryButton( - isEnabled = primaryButtonEnabled, - isLoading = primaryButtonLoading, - label = primaryButtonText, - onButtonClick = { - focusManager.clearFocus() - onPrimaryButtonClick() - }, - canClickWhileDisabled = !primaryButtonLoading, - onDisabledButtonClick = { - focusManager.clearFocus() - onDisabledButtonClick() - }, - modifier = Modifier.padding(vertical = 16.dp), + Text( + title, + style = MaterialTheme.typography.h4, + modifier = Modifier.padding(bottom = 8.dp) + ) + topContent() + formContent() + bottomContent() + saveError?.let { + ErrorMessage( + error = it.resolve(), + modifier = Modifier.padding(top = 2.dp), ) } + PrimaryButton( + isEnabled = primaryButtonEnabled, + isLoading = primaryButtonLoading, + label = primaryButtonText, + onButtonClick = { + focusManager.clearFocus() + onPrimaryButtonClick() + }, + canClickWhileDisabled = !primaryButtonLoading, + onDisabledButtonClick = { + focusManager.clearFocus() + onDisabledButtonClick() + }, + modifier = Modifier.padding(vertical = 16.dp), + ) } } } @@ -142,7 +137,6 @@ internal fun InputAddressScreen( val billingSameAsShippingState by viewModel.shippingSameAsBillingState.collectAsState() InputAddressScreen( - appearance = viewModel.args.config?.appearance ?: PaymentSheet.Appearance(), primaryButtonEnabled = completeValues != null && formEnabled, primaryButtonLoading = !formEnabled, closeButtonEnabled = formEnabled, diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityTest.kt index d2b981ce150..e75735e3e64 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityTest.kt @@ -4,7 +4,13 @@ import android.app.Activity import android.app.Application import android.content.Intent import android.os.Bundle +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.SemanticsActions +import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.junit4.createEmptyComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performSemanticsAction +import androidx.compose.ui.text.TextLayoutResult import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.test.core.app.ActivityScenario @@ -12,19 +18,25 @@ import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFixtures.DEFAULT_API_CONFIG import com.stripe.android.networktesting.NetworkRule +import com.stripe.android.paymentelement.AppearanceAPIAdditionsPreview import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.R +import com.stripe.android.paymentsheet.parseAppearance import com.stripe.android.paymentsheet.repositories.CheckoutSessionResponse import com.stripe.android.paymentsheet.repositories.CheckoutSessionResponseFactory import com.stripe.android.testing.createComposeCleanupRule +import com.stripe.android.ui.core.elements.autocomplete.PlacesClientProxy import com.stripe.paymentelementtestpages.AddressElementPage import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain import org.junit.runner.RunWith +import org.mockito.kotlin.mock import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) +@OptIn(AppearanceAPIAdditionsPreview::class) internal class AddressElementActivityTest { private val applicationContext = ApplicationProvider.getApplicationContext() private val composeTestRule = createEmptyComposeRule() @@ -187,6 +199,24 @@ internal class AddressElementActivityTest { assertThat(parsed).isEqualTo(AddressElementActivityContract.Result.Canceled) } + @Test + @Config(qualifiers = "notnight") + fun `always dark appearance is applied to autocomplete text on a light system`() { + assertAutocompletePrimaryColor( + themeMode = PaymentSheet.ThemeMode.AlwaysDark, + expectedPrimary = DARK_PRIMARY, + ) + } + + @Test + @Config(qualifiers = "night") + fun `always light appearance is applied to autocomplete text on a dark system`() { + assertAutocompletePrimaryColor( + themeMode = PaymentSheet.ThemeMode.AlwaysLight, + expectedPrimary = LIGHT_PRIMARY, + ) + } + @Test fun `standalone shipping returns the entered address without a tax update`() = runScenario( args = AddressElementActivityContract.Args.Standalone( @@ -420,7 +450,58 @@ internal class AddressElementActivityTest { block: AddressElementActivityTestRunner.Scenario.() -> Unit, ) = activityTestRunner.run(args, block) + private fun assertAutocompletePrimaryColor( + themeMode: PaymentSheet.ThemeMode, + expectedPrimary: Color, + ) { + val previousPlacesClientOverride = PlacesClientProxy.override + + try { + PlacesClientProxy.override = mock() + runScenario( + args = AddressElementActivityContract.Args.Standalone( + apiConfiguration = DEFAULT_API_CONFIG, + config = AddressLauncher.Configuration.Builder() + .appearance(createAppearance(themeMode)) + .build(), + ), + ) { + composeTestRule.waitForIdle() + activityScenario.onActivity { activity -> + ViewModelProvider(activity)[AddressElementViewModel::class.java] + .navigator + .navigateTo(AddressElementScreen.Autocomplete(country = "US")) + } + composeTestRule.waitForIdle() + + val textLayoutResult = composeTestRule.textLayoutResultFor( + applicationContext.getString(R.string.stripe_paymentsheet_enter_address_manually) + ) + assertThat(textLayoutResult.layoutInput.style.color).isEqualTo(expectedPrimary) + } + } finally { + PlacesClientProxy.override = previousPlacesClientOverride + PaymentSheet.Appearance().parseAppearance() + } + } + + private fun createAppearance(themeMode: PaymentSheet.ThemeMode) = PaymentSheet.Appearance( + colorsLight = PaymentSheet.Colors.configureDefaultLight(primary = LIGHT_PRIMARY), + colorsDark = PaymentSheet.Colors.Builder.dark().primary(DARK_PRIMARY).build(), + themeMode = themeMode, + ) + + private fun ComposeTestRule.textLayoutResultFor(text: String): TextLayoutResult { + val results = mutableListOf() + onNodeWithText(text).performSemanticsAction(SemanticsActions.GetTextLayoutResult) { action -> + action(results) + } + return results.single() + } + private companion object { + val LIGHT_PRIMARY = Color(0xFF123456) + val DARK_PRIMARY = Color(0xFF654321) val SHIPPING_ADDRESS = AddressDetails( name = "Jenny Rosen", address = PaymentSheet.Address( diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/InputAddressScreenScreenshotTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/InputAddressScreenScreenshotTest.kt index 10de645f50c..113cf5be8f2 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/InputAddressScreenScreenshotTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/InputAddressScreenScreenshotTest.kt @@ -21,6 +21,7 @@ import com.stripe.android.core.strings.ResolvableString import com.stripe.android.core.strings.resolvableString import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.R +import com.stripe.android.paymentsheet.ui.PaymentElementTheme import com.stripe.android.screenshottesting.PaparazziRule import com.stripe.android.screenshottesting.SystemAppearance import com.stripe.android.ui.core.FormUI @@ -137,31 +138,32 @@ internal class InputAddressScreenScreenshotTest { ) } - InputAddressScreen( - appearance = appearance, - primaryButtonEnabled = addressFormController.completeFormValues.value != null, - primaryButtonLoading = false, - closeButtonEnabled = true, - primaryButtonText = primaryButtonText, - title = title, - onPrimaryButtonClick = {}, - onDisabledButtonClick = {}, - onCloseClick = {}, - topContent = {}, - formContent = { - if (scrollToBottom) { - ScrollToBottom() - } - FormUI( - hiddenIdentifiers = emptySet(), - enabled = true, - elements = addressFormController.elements, - lastTextFieldIdentifier = addressFormController.lastTextFieldIdentifier.value, - ) - }, - bottomContent = {}, - saveError = null, - ) + PaymentElementTheme(appearance = appearance) { + InputAddressScreen( + primaryButtonEnabled = addressFormController.completeFormValues.value != null, + primaryButtonLoading = false, + closeButtonEnabled = true, + primaryButtonText = primaryButtonText, + title = title, + onPrimaryButtonClick = {}, + onDisabledButtonClick = {}, + onCloseClick = {}, + topContent = {}, + formContent = { + if (scrollToBottom) { + ScrollToBottom() + } + FormUI( + hiddenIdentifiers = emptySet(), + enabled = true, + elements = addressFormController.elements, + lastTextFieldIdentifier = addressFormController.lastTextFieldIdentifier.value, + ) + }, + bottomContent = {}, + saveError = null, + ) + } } @Composable @@ -171,28 +173,29 @@ internal class InputAddressScreenScreenshotTest { ) { val completeValues by formController.completeFormValues.collectAsState() - InputAddressScreen( - appearance = PaymentSheet.Appearance(), - primaryButtonEnabled = completeValues != null, - primaryButtonLoading = false, - closeButtonEnabled = true, - primaryButtonText = stringResource(R.string.stripe_paymentsheet_address_element_primary_button), - title = stringResource(R.string.stripe_paymentsheet_address_element_shipping_address), - onPrimaryButtonClick = {}, - onDisabledButtonClick = {}, - onCloseClick = {}, - topContent = {}, - formContent = { - FormUI( - hiddenIdentifiers = emptySet(), - enabled = true, - elements = formController.elements, - lastTextFieldIdentifier = formController.lastTextFieldIdentifier.value, - ) - }, - bottomContent = {}, - saveError = saveError, - ) + PaymentElementTheme(appearance = PaymentSheet.Appearance()) { + InputAddressScreen( + primaryButtonEnabled = completeValues != null, + primaryButtonLoading = false, + closeButtonEnabled = true, + primaryButtonText = stringResource(R.string.stripe_paymentsheet_address_element_primary_button), + title = stringResource(R.string.stripe_paymentsheet_address_element_shipping_address), + onPrimaryButtonClick = {}, + onDisabledButtonClick = {}, + onCloseClick = {}, + topContent = {}, + formContent = { + FormUI( + hiddenIdentifiers = emptySet(), + enabled = true, + elements = formController.elements, + lastTextFieldIdentifier = formController.lastTextFieldIdentifier.value, + ) + }, + bottomContent = {}, + saveError = saveError, + ) + } } private fun createFilledFormController() = AddressFormController( From ab5cc073cc78edda08a276fb332e7ff9e8406b5c Mon Sep 17 00:00:00 2001 From: Chengtin Tsai Date: Fri, 16 Oct 2026 10:05:57 -0700 Subject: [PATCH 2/2] Keep one Activity theme wiring regression Committed-By-Agent: codex Co-authored-by: codex --- .../AddressElementActivityTest.kt | 96 +++++++------------ 1 file changed, 34 insertions(+), 62 deletions(-) diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityTest.kt index e75735e3e64..4b9fb1bf75e 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/AddressElementActivityTest.kt @@ -6,7 +6,6 @@ import android.content.Intent import android.os.Bundle import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.SemanticsActions -import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performSemanticsAction @@ -202,19 +201,43 @@ internal class AddressElementActivityTest { @Test @Config(qualifiers = "notnight") fun `always dark appearance is applied to autocomplete text on a light system`() { - assertAutocompletePrimaryColor( + val expectedPrimary = Color(0xFF654321) + val appearance = PaymentSheet.Appearance( + colorsDark = PaymentSheet.Colors.configureDefaultDark(primary = expectedPrimary), themeMode = PaymentSheet.ThemeMode.AlwaysDark, - expectedPrimary = DARK_PRIMARY, ) - } + val previousPlacesClientOverride = PlacesClientProxy.override - @Test - @Config(qualifiers = "night") - fun `always light appearance is applied to autocomplete text on a dark system`() { - assertAutocompletePrimaryColor( - themeMode = PaymentSheet.ThemeMode.AlwaysLight, - expectedPrimary = LIGHT_PRIMARY, - ) + try { + PlacesClientProxy.override = mock() + runScenario( + args = AddressElementActivityContract.Args.Standalone( + apiConfiguration = DEFAULT_API_CONFIG, + config = AddressLauncher.Configuration.Builder() + .appearance(appearance) + .build(), + ), + ) { + composeTestRule.waitForIdle() + activityScenario.onActivity { activity -> + ViewModelProvider(activity)[AddressElementViewModel::class.java] + .navigator + .navigateTo(AddressElementScreen.Autocomplete(country = "US")) + } + composeTestRule.waitForIdle() + + val textLayoutResults = mutableListOf() + composeTestRule.onNodeWithText( + applicationContext.getString(R.string.stripe_paymentsheet_enter_address_manually) + ).performSemanticsAction(SemanticsActions.GetTextLayoutResult) { action -> + action(textLayoutResults) + } + assertThat(textLayoutResults.single().layoutInput.style.color).isEqualTo(expectedPrimary) + } + } finally { + PlacesClientProxy.override = previousPlacesClientOverride + PaymentSheet.Appearance().parseAppearance() + } } @Test @@ -450,58 +473,7 @@ internal class AddressElementActivityTest { block: AddressElementActivityTestRunner.Scenario.() -> Unit, ) = activityTestRunner.run(args, block) - private fun assertAutocompletePrimaryColor( - themeMode: PaymentSheet.ThemeMode, - expectedPrimary: Color, - ) { - val previousPlacesClientOverride = PlacesClientProxy.override - - try { - PlacesClientProxy.override = mock() - runScenario( - args = AddressElementActivityContract.Args.Standalone( - apiConfiguration = DEFAULT_API_CONFIG, - config = AddressLauncher.Configuration.Builder() - .appearance(createAppearance(themeMode)) - .build(), - ), - ) { - composeTestRule.waitForIdle() - activityScenario.onActivity { activity -> - ViewModelProvider(activity)[AddressElementViewModel::class.java] - .navigator - .navigateTo(AddressElementScreen.Autocomplete(country = "US")) - } - composeTestRule.waitForIdle() - - val textLayoutResult = composeTestRule.textLayoutResultFor( - applicationContext.getString(R.string.stripe_paymentsheet_enter_address_manually) - ) - assertThat(textLayoutResult.layoutInput.style.color).isEqualTo(expectedPrimary) - } - } finally { - PlacesClientProxy.override = previousPlacesClientOverride - PaymentSheet.Appearance().parseAppearance() - } - } - - private fun createAppearance(themeMode: PaymentSheet.ThemeMode) = PaymentSheet.Appearance( - colorsLight = PaymentSheet.Colors.configureDefaultLight(primary = LIGHT_PRIMARY), - colorsDark = PaymentSheet.Colors.Builder.dark().primary(DARK_PRIMARY).build(), - themeMode = themeMode, - ) - - private fun ComposeTestRule.textLayoutResultFor(text: String): TextLayoutResult { - val results = mutableListOf() - onNodeWithText(text).performSemanticsAction(SemanticsActions.GetTextLayoutResult) { action -> - action(results) - } - return results.single() - } - private companion object { - val LIGHT_PRIMARY = Color(0xFF123456) - val DARK_PRIMARY = Color(0xFF654321) val SHIPPING_ADDRESS = AddressDetails( name = "Jenny Rosen", address = PaymentSheet.Address(