Skip to content

Commit 578cea9

Browse files
toluo-stripecodex
andcommitted
Add semantic state to StripeTheme
Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com>
1 parent 20967b3 commit 578cea9

7 files changed

Lines changed: 208 additions & 2 deletions

stripe-ui-core/src/main/java/com/stripe/android/uicore/StripeTheme.kt

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,9 @@ val LocalSectionStyle = staticCompositionLocalOf { StripeTheme.sectionStyle }
488488
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
489489
val LocalTextFieldInsets = staticCompositionLocalOf { StripeTheme.textFieldInsets }
490490

491+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
492+
private val LocalStripeThemeIsDark = staticCompositionLocalOf<Boolean?> { null }
493+
491494
/**
492495
* Base Theme for Stripe Composables.
493496
* CAUTION: This theme is mutable by merchant configurations. You shouldn't be passing colors,
@@ -504,6 +507,35 @@ fun StripeTheme(
504507
textFieldInsets: FormInsets = StripeTheme.textFieldInsets,
505508
iconStyle: IconStyle = StripeTheme.iconStyle,
506509
content: @Composable () -> Unit
510+
) {
511+
StripeTheme(
512+
isDark = isSystemInDarkTheme(),
513+
colors = colors,
514+
shapes = shapes,
515+
typography = typography,
516+
sectionSpacing = sectionSpacing,
517+
sectionStyle = sectionStyle,
518+
textFieldInsets = textFieldInsets,
519+
iconStyle = iconStyle,
520+
content = content,
521+
)
522+
}
523+
524+
/**
525+
* Base Theme for Stripe Composables whose light or dark appearance is supplied by their integration.
526+
*/
527+
@Composable
528+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
529+
fun StripeTheme(
530+
isDark: Boolean,
531+
colors: StripeColors,
532+
shapes: StripeShapes,
533+
typography: StripeTypography,
534+
sectionSpacing: Float?,
535+
sectionStyle: SectionStyle,
536+
textFieldInsets: FormInsets,
537+
iconStyle: IconStyle,
538+
content: @Composable () -> Unit,
507539
) {
508540
val isRobolectricTest = runCatching {
509541
BuildConfig.DEBUG && Build.FINGERPRINT.lowercase() == "robolectric"
@@ -528,6 +560,7 @@ fun StripeTheme(
528560
LocalSectionSpacing provides sectionSpacing,
529561
LocalSectionStyle provides sectionStyle,
530562
LocalTextFieldInsets provides textFieldInsets,
563+
LocalStripeThemeIsDark provides isDark,
531564
LocalIconStyle provides iconStyle,
532565
LocalInspectionMode provides inspectionMode,
533566
LocalInstrumentationTest provides isInstrumentationTest,
@@ -557,14 +590,16 @@ fun StripeTheme(
557590
fun DefaultStripeTheme(
558591
content: @Composable () -> Unit
559592
) {
560-
val colors = StripeThemeDefaults.colors(isSystemInDarkTheme())
593+
val isDark = isSystemInDarkTheme()
594+
val colors = StripeThemeDefaults.colors(isDark)
561595
val shapes = StripeThemeDefaults.shapes
562596
val typography = StripeThemeDefaults.typography
563597

564598
CompositionLocalProvider(
565599
LocalColors provides colors,
566600
LocalShapes provides shapes,
567-
LocalTypography provides typography
601+
LocalTypography provides typography,
602+
LocalStripeThemeIsDark provides isDark,
568603
) {
569604
MaterialTheme(
570605
colors = colors.materialColors,
@@ -594,6 +629,13 @@ val MaterialTheme.stripeTypography: StripeTypography
594629
@ReadOnlyComposable
595630
get() = LocalTypography.current
596631

632+
@Suppress("UnusedReceiverParameter")
633+
val MaterialTheme.stripeThemeIsDark: Boolean
634+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
635+
@Composable
636+
@ReadOnlyComposable
637+
get() = LocalStripeThemeIsDark.current ?: isSystemInDarkTheme()
638+
597639
@Composable
598640
@ReadOnlyComposable
599641
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.stripe.android.uicore
2+
3+
import androidx.compose.foundation.layout.Column
4+
import androidx.compose.foundation.layout.Spacer
5+
import androidx.compose.foundation.layout.fillMaxWidth
6+
import androidx.compose.foundation.layout.height
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.material.MaterialTheme
9+
import androidx.compose.material.Surface
10+
import androidx.compose.material.Text
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.unit.dp
13+
import com.stripe.android.screenshottesting.PaparazziRule
14+
import com.stripe.android.screenshottesting.SystemAppearance
15+
import org.junit.Rule
16+
import org.junit.Test
17+
18+
internal class StripeThemeScreenshotTest {
19+
@get:Rule
20+
val paparazziRule = PaparazziRule(
21+
SystemAppearance.entries,
22+
boxModifier = Modifier.fillMaxWidth(),
23+
includeStripeTheme = false,
24+
)
25+
26+
@Test
27+
fun explicitDarkTheme() {
28+
snapshotTheme(isDark = true)
29+
}
30+
31+
@Test
32+
fun explicitLightTheme() {
33+
snapshotTheme(isDark = false)
34+
}
35+
36+
private fun snapshotTheme(isDark: Boolean) {
37+
paparazziRule.snapshot {
38+
StripeTheme(
39+
isDark = isDark,
40+
colors = StripeThemeDefaults.colors(isDark),
41+
shapes = StripeThemeDefaults.shapes,
42+
typography = StripeThemeDefaults.typography,
43+
sectionSpacing = StripeThemeDefaults.sectionSpacing,
44+
sectionStyle = StripeThemeDefaults.sectionStyle,
45+
textFieldInsets = StripeThemeDefaults.textFieldInsets,
46+
iconStyle = StripeThemeDefaults.iconStyle,
47+
) {
48+
Surface(
49+
color = MaterialTheme.colors.surface,
50+
modifier = Modifier.fillMaxWidth(),
51+
) {
52+
Column(modifier = Modifier.padding(16.dp)) {
53+
Text(
54+
text = "Payment details",
55+
color = MaterialTheme.colors.onSurface,
56+
style = MaterialTheme.typography.h6,
57+
)
58+
Spacer(modifier = Modifier.height(8.dp))
59+
Text(
60+
text = if (MaterialTheme.stripeThemeIsDark) {
61+
"Dark appearance"
62+
} else {
63+
"Light appearance"
64+
},
65+
color = MaterialTheme.stripeColors.subtitle,
66+
style = MaterialTheme.typography.body2,
67+
)
68+
}
69+
}
70+
}
71+
}
72+
}
73+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.stripe.android.uicore
2+
3+
import android.os.Build
4+
import androidx.compose.material.MaterialTheme
5+
import androidx.compose.ui.test.junit4.createComposeRule
6+
import com.google.common.truth.Truth.assertThat
7+
import com.stripe.android.testing.createComposeCleanupRule
8+
import org.junit.Rule
9+
import org.junit.Test
10+
import org.junit.runner.RunWith
11+
import org.robolectric.RobolectricTestRunner
12+
import org.robolectric.annotation.Config
13+
14+
@RunWith(RobolectricTestRunner::class)
15+
@Config(sdk = [Build.VERSION_CODES.Q])
16+
internal class StripeThemeTest {
17+
@get:Rule
18+
val composeRule = createComposeRule()
19+
20+
@get:Rule
21+
val composeCleanupRule = createComposeCleanupRule()
22+
23+
@Test
24+
fun `explicit dark theme is provided to descendants`() = runScenario(isDark = true) {
25+
assertThat(isDark).isTrue()
26+
}
27+
28+
@Test
29+
fun `explicit light theme is provided to descendants`() = runScenario(isDark = false) {
30+
assertThat(isDark).isFalse()
31+
}
32+
33+
@Test
34+
@Config(qualifiers = "night")
35+
fun `legacy theme delegates with system dark mode`() {
36+
var isDark: Boolean? = null
37+
38+
composeRule.setContent {
39+
StripeTheme {
40+
isDark = MaterialTheme.stripeThemeIsDark
41+
}
42+
}
43+
composeRule.waitForIdle()
44+
45+
assertThat(isDark).isTrue()
46+
}
47+
48+
@Test
49+
@Config(qualifiers = "notnight")
50+
fun `legacy theme delegates with system light mode`() {
51+
var isDark: Boolean? = null
52+
53+
composeRule.setContent {
54+
StripeTheme {
55+
isDark = MaterialTheme.stripeThemeIsDark
56+
}
57+
}
58+
composeRule.waitForIdle()
59+
60+
assertThat(isDark).isFalse()
61+
}
62+
63+
private fun runScenario(
64+
isDark: Boolean,
65+
block: Scenario.() -> Unit,
66+
) {
67+
var providedIsDark: Boolean? = null
68+
69+
composeRule.setContent {
70+
StripeTheme(
71+
isDark = isDark,
72+
colors = StripeThemeDefaults.colors(isDark),
73+
shapes = StripeThemeDefaults.shapes,
74+
typography = StripeThemeDefaults.typography,
75+
sectionSpacing = StripeThemeDefaults.sectionSpacing,
76+
sectionStyle = StripeThemeDefaults.sectionStyle,
77+
textFieldInsets = StripeThemeDefaults.textFieldInsets,
78+
iconStyle = StripeThemeDefaults.iconStyle,
79+
) {
80+
providedIsDark = MaterialTheme.stripeThemeIsDark
81+
}
82+
}
83+
composeRule.waitForIdle()
84+
85+
Scenario(isDark = requireNotNull(providedIsDark)).apply(block)
86+
}
87+
88+
private data class Scenario(
89+
val isDark: Boolean,
90+
)
91+
}
8.66 KB
Loading
8.66 KB
Loading
8.86 KB
Loading
8.86 KB
Loading

0 commit comments

Comments
 (0)