@stripe/stripe-react-native 0.57.1 → 0.57.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +0 -8
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/java/com/reactnativestripesdk/EmbeddedPaymentElementView.kt +49 -0
  4. package/android/src/main/java/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt +61 -0
  5. package/android/src/main/java/com/reactnativestripesdk/EventEmitterCompat.kt +4 -0
  6. package/android/src/main/java/com/reactnativestripesdk/PaymentSheetManager.kt +131 -30
  7. package/android/src/main/java/com/reactnativestripesdk/customersheet/CustomerSheetManager.kt +38 -13
  8. package/android/src/oldarch/java/com/facebook/react/viewmanagers/EmbeddedPaymentElementViewManagerDelegate.java +3 -0
  9. package/android/src/oldarch/java/com/facebook/react/viewmanagers/EmbeddedPaymentElementViewManagerInterface.java +2 -0
  10. package/android/src/test/java/com/reactnativestripesdk/DrawableConversionPropertyTest.kt +224 -0
  11. package/android/src/test/java/com/reactnativestripesdk/DrawableConversionTest.kt +146 -0
  12. package/android/src/test/java/com/reactnativestripesdk/DrawableLoadingTest.kt +150 -0
  13. package/android/src/test/java/com/reactnativestripesdk/PaymentOptionImageConsistencyTest.kt +186 -0
  14. package/lib/commonjs/components/AddToWalletButton.js +1 -1
  15. package/lib/commonjs/components/AddToWalletButton.js.map +1 -1
  16. package/lib/commonjs/components/AddressSheet.js +1 -1
  17. package/lib/commonjs/components/AddressSheet.js.map +1 -1
  18. package/lib/commonjs/components/AuBECSDebitForm.js +1 -1
  19. package/lib/commonjs/components/AuBECSDebitForm.js.map +1 -1
  20. package/lib/commonjs/components/CardField.js +1 -1
  21. package/lib/commonjs/components/CardField.js.map +1 -1
  22. package/lib/commonjs/components/CardForm.js +1 -1
  23. package/lib/commonjs/components/CardForm.js.map +1 -1
  24. package/lib/commonjs/components/PlatformPayButton.js +1 -1
  25. package/lib/commonjs/components/PlatformPayButton.js.map +1 -1
  26. package/lib/commonjs/components/StripeContainer.js +1 -1
  27. package/lib/commonjs/components/StripeContainer.js.map +1 -1
  28. package/lib/commonjs/connect/Components.js +1 -1
  29. package/lib/commonjs/connect/Components.js.map +1 -1
  30. package/lib/commonjs/connect/ConnectComponentsProvider.js +1 -1
  31. package/lib/commonjs/connect/ConnectComponentsProvider.js.map +1 -1
  32. package/lib/commonjs/connect/EmbeddedComponent.js +1 -1
  33. package/lib/commonjs/connect/EmbeddedComponent.js.map +1 -1
  34. package/lib/commonjs/connect/ModalCloseButton.js +1 -1
  35. package/lib/commonjs/connect/ModalCloseButton.js.map +1 -1
  36. package/lib/commonjs/connect/NavigationBar.js +1 -1
  37. package/lib/commonjs/connect/NavigationBar.js.map +1 -1
  38. package/lib/commonjs/events.js.map +1 -1
  39. package/lib/commonjs/helpers.js +1 -1
  40. package/lib/commonjs/hooks/useOnramp.js +1 -1
  41. package/lib/commonjs/hooks/useOnramp.js.map +1 -1
  42. package/lib/commonjs/specs/NativeAddToWalletButton.js +1 -1
  43. package/lib/commonjs/specs/NativeAddressSheet.js +1 -1
  44. package/lib/commonjs/specs/NativeApplePayButton.js +1 -1
  45. package/lib/commonjs/specs/NativeAuBECSDebitForm.js +1 -1
  46. package/lib/commonjs/specs/NativeCardField.js +1 -1
  47. package/lib/commonjs/specs/NativeCardField.js.map +1 -1
  48. package/lib/commonjs/specs/NativeCardForm.js +1 -1
  49. package/lib/commonjs/specs/NativeCardForm.js.map +1 -1
  50. package/lib/commonjs/specs/NativeConnectAccountOnboardingView.js +1 -1
  51. package/lib/commonjs/specs/NativeEmbeddedPaymentElement.js +1 -1
  52. package/lib/commonjs/specs/NativeEmbeddedPaymentElement.js.map +1 -1
  53. package/lib/commonjs/specs/NativeGooglePayButton.js +1 -1
  54. package/lib/commonjs/specs/NativeNavigationBar.js +1 -1
  55. package/lib/commonjs/specs/NativeStripeContainer.js +1 -1
  56. package/lib/commonjs/types/EmbeddedPaymentElement.js +1 -1
  57. package/lib/commonjs/types/EmbeddedPaymentElement.js.map +1 -1
  58. package/lib/module/components/AddToWalletButton.js +1 -1
  59. package/lib/module/components/AddToWalletButton.js.map +1 -1
  60. package/lib/module/components/AddressSheet.js +1 -1
  61. package/lib/module/components/AddressSheet.js.map +1 -1
  62. package/lib/module/components/AuBECSDebitForm.js +1 -1
  63. package/lib/module/components/AuBECSDebitForm.js.map +1 -1
  64. package/lib/module/components/CardField.js +1 -1
  65. package/lib/module/components/CardField.js.map +1 -1
  66. package/lib/module/components/CardForm.js +1 -1
  67. package/lib/module/components/CardForm.js.map +1 -1
  68. package/lib/module/components/PlatformPayButton.js +1 -1
  69. package/lib/module/components/PlatformPayButton.js.map +1 -1
  70. package/lib/module/components/StripeContainer.js +1 -1
  71. package/lib/module/components/StripeContainer.js.map +1 -1
  72. package/lib/module/connect/Components.js +1 -1
  73. package/lib/module/connect/Components.js.map +1 -1
  74. package/lib/module/connect/ConnectComponentsProvider.js +1 -1
  75. package/lib/module/connect/ConnectComponentsProvider.js.map +1 -1
  76. package/lib/module/connect/EmbeddedComponent.js +1 -1
  77. package/lib/module/connect/EmbeddedComponent.js.map +1 -1
  78. package/lib/module/connect/ModalCloseButton.js +1 -1
  79. package/lib/module/connect/ModalCloseButton.js.map +1 -1
  80. package/lib/module/connect/NavigationBar.js +1 -1
  81. package/lib/module/connect/NavigationBar.js.map +1 -1
  82. package/lib/module/events.js.map +1 -1
  83. package/lib/module/helpers.js +1 -1
  84. package/lib/module/hooks/useOnramp.js +1 -1
  85. package/lib/module/hooks/useOnramp.js.map +1 -1
  86. package/lib/module/specs/NativeAddToWalletButton.js +1 -1
  87. package/lib/module/specs/NativeAddressSheet.js +1 -1
  88. package/lib/module/specs/NativeApplePayButton.js +1 -1
  89. package/lib/module/specs/NativeAuBECSDebitForm.js +1 -1
  90. package/lib/module/specs/NativeCardField.js +1 -1
  91. package/lib/module/specs/NativeCardField.js.map +1 -1
  92. package/lib/module/specs/NativeCardForm.js +1 -1
  93. package/lib/module/specs/NativeCardForm.js.map +1 -1
  94. package/lib/module/specs/NativeConnectAccountOnboardingView.js +1 -1
  95. package/lib/module/specs/NativeEmbeddedPaymentElement.js +1 -1
  96. package/lib/module/specs/NativeEmbeddedPaymentElement.js.map +1 -1
  97. package/lib/module/specs/NativeGooglePayButton.js +1 -1
  98. package/lib/module/specs/NativeNavigationBar.js +1 -1
  99. package/lib/module/specs/NativeStripeContainer.js +1 -1
  100. package/lib/module/types/EmbeddedPaymentElement.js +1 -1
  101. package/lib/module/types/EmbeddedPaymentElement.js.map +1 -1
  102. package/lib/typescript/src/connect/EmbeddedComponent.d.ts.map +1 -1
  103. package/lib/typescript/src/events.d.ts +1 -0
  104. package/lib/typescript/src/events.d.ts.map +1 -1
  105. package/lib/typescript/src/hooks/useOnramp.d.ts +2 -1
  106. package/lib/typescript/src/hooks/useOnramp.d.ts.map +1 -1
  107. package/lib/typescript/src/specs/NativeEmbeddedPaymentElement.d.ts +1 -0
  108. package/lib/typescript/src/specs/NativeEmbeddedPaymentElement.d.ts.map +1 -1
  109. package/lib/typescript/src/types/EmbeddedPaymentElement.d.ts.map +1 -1
  110. package/package.json +13 -7
  111. package/src/connect/EmbeddedComponent.tsx +10 -10
  112. package/src/events.ts +1 -0
  113. package/src/hooks/useOnramp.tsx +5 -1
  114. package/src/specs/NativeEmbeddedPaymentElement.ts +5 -1
  115. package/src/types/EmbeddedPaymentElement.tsx +24 -3
@@ -0,0 +1,224 @@
1
+ package com.reactnativestripesdk
2
+
3
+ import android.graphics.Color
4
+ import android.graphics.drawable.ColorDrawable
5
+ import kotlinx.coroutines.test.runTest
6
+ import org.junit.Assert.assertEquals
7
+ import org.junit.Assert.assertNotEquals
8
+ import org.junit.Assert.assertNotNull
9
+ import org.junit.Assert.assertTrue
10
+ import org.junit.Test
11
+ import org.junit.runner.RunWith
12
+ import org.robolectric.RobolectricTestRunner
13
+ import kotlin.random.Random
14
+
15
+ /**
16
+ * Property-based tests for drawable conversion.
17
+ * These tests verify mathematical properties that should always hold true.
18
+ */
19
+ @RunWith(RobolectricTestRunner::class)
20
+ class DrawableConversionPropertyTest {
21
+ @Test
22
+ fun `PROPERTY - conversion is idempotent`() =
23
+ runTest {
24
+ // Property: Converting the same drawable multiple times yields same result
25
+ repeat(20) {
26
+ val color =
27
+ Color.rgb(
28
+ Random.nextInt(256),
29
+ Random.nextInt(256),
30
+ Random.nextInt(256),
31
+ )
32
+ val width = Random.nextInt(50, 200)
33
+ val height = Random.nextInt(50, 200)
34
+
35
+ val drawable =
36
+ ColorDrawable(color).apply {
37
+ setBounds(0, 0, width, height)
38
+ }
39
+
40
+ val result1 = convertDrawableToBase64(drawable)
41
+ val result2 = convertDrawableToBase64(drawable)
42
+
43
+ assertEquals(
44
+ "Same drawable should always produce same base64 (iteration $it, ${width}x$height)",
45
+ result1,
46
+ result2,
47
+ )
48
+ }
49
+ }
50
+
51
+ @Test
52
+ fun `PROPERTY - different drawables produce different base64`() =
53
+ runTest {
54
+ val drawable1 =
55
+ ColorDrawable(Color.RED).apply {
56
+ setBounds(0, 0, 100, 100)
57
+ }
58
+ val drawable2 =
59
+ ColorDrawable(Color.BLUE).apply {
60
+ setBounds(0, 0, 100, 100)
61
+ }
62
+
63
+ val result1 = convertDrawableToBase64(drawable1)
64
+ val result2 = convertDrawableToBase64(drawable2)
65
+
66
+ assertNotEquals(
67
+ "Different colored drawables should produce different base64",
68
+ result1,
69
+ result2,
70
+ )
71
+ }
72
+
73
+ @Test
74
+ fun `PROPERTY - base64 length scales with drawable size`() =
75
+ runTest {
76
+ val small =
77
+ ColorDrawable(Color.RED).apply {
78
+ setBounds(0, 0, 50, 50)
79
+ }
80
+ val medium =
81
+ ColorDrawable(Color.RED).apply {
82
+ setBounds(0, 0, 100, 100)
83
+ }
84
+ val large =
85
+ ColorDrawable(Color.RED).apply {
86
+ setBounds(0, 0, 200, 200)
87
+ }
88
+
89
+ val smallResult = convertDrawableToBase64(small)
90
+ val mediumResult = convertDrawableToBase64(medium)
91
+ val largeResult = convertDrawableToBase64(large)
92
+
93
+ assertNotNull(smallResult)
94
+ assertNotNull(mediumResult)
95
+ assertNotNull(largeResult)
96
+
97
+ assertTrue(
98
+ "Medium drawable should produce longer base64 than small",
99
+ mediumResult!!.length > smallResult!!.length,
100
+ )
101
+ assertTrue(
102
+ "Large drawable should produce longer base64 than medium",
103
+ largeResult!!.length > mediumResult.length,
104
+ )
105
+ }
106
+
107
+ @Test
108
+ fun `PROPERTY - conversion preserves drawable dimensions`() =
109
+ runTest {
110
+ val testSizes =
111
+ listOf(
112
+ Pair(50, 50),
113
+ Pair(100, 75),
114
+ Pair(147, 105), // Typical card icon
115
+ Pair(200, 150),
116
+ Pair(250, 200),
117
+ )
118
+
119
+ testSizes.forEach { (width, height) ->
120
+ val drawable =
121
+ ColorDrawable(Color.GREEN).apply {
122
+ setBounds(0, 0, width, height)
123
+ }
124
+
125
+ val bitmap = getBitmapFromDrawable(drawable)
126
+
127
+ assertNotNull("Bitmap should not be null for ${width}x$height", bitmap)
128
+ assertEquals("Width should be preserved for ${width}x$height", width, bitmap!!.width)
129
+ assertEquals("Height should be preserved for ${width}x$height", height, bitmap.height)
130
+ }
131
+ }
132
+
133
+ @Test
134
+ fun `PROPERTY - null input produces null output consistently`() =
135
+ runTest {
136
+ // Invalid drawables (no bounds set) should consistently return null
137
+ val invalidDrawable1 = ColorDrawable(Color.RED)
138
+ val invalidDrawable2 = ColorDrawable(Color.BLUE)
139
+
140
+ val result1 = convertDrawableToBase64(invalidDrawable1)
141
+ val result2 = convertDrawableToBase64(invalidDrawable2)
142
+
143
+ assertNotNull("Result1 should be null for invalid drawable", result1 == null)
144
+ assertNotNull("Result2 should be null for invalid drawable", result2 == null)
145
+ }
146
+
147
+ @Test
148
+ fun `PROPERTY - aspect ratio is preserved in bitmap`() {
149
+ val testCases =
150
+ listOf(
151
+ Triple(100, 100, 1.0), // Square
152
+ Triple(200, 100, 2.0), // 2:1
153
+ Triple(147, 105, 1.4), // Card icon ratio
154
+ Triple(100, 200, 0.5), // Portrait
155
+ )
156
+
157
+ testCases.forEach { (width, height, expectedRatio) ->
158
+ val drawable =
159
+ ColorDrawable(Color.YELLOW).apply {
160
+ setBounds(0, 0, width, height)
161
+ }
162
+
163
+ val bitmap = getBitmapFromDrawable(drawable)
164
+
165
+ assertNotNull(bitmap)
166
+ val actualRatio = bitmap!!.width.toDouble() / bitmap.height.toDouble()
167
+ assertEquals(
168
+ "Aspect ratio should be preserved for ${width}x$height",
169
+ expectedRatio,
170
+ actualRatio,
171
+ 0.01, // Allow small floating point error
172
+ )
173
+ }
174
+ }
175
+
176
+ @Test
177
+ fun `PROPERTY - conversion is deterministic across multiple runs`() =
178
+ runTest {
179
+ // Same drawable, converted multiple times in sequence, should always yield same result
180
+ val drawable =
181
+ ColorDrawable(Color.CYAN).apply {
182
+ setBounds(0, 0, 120, 90)
183
+ }
184
+
185
+ val results = (1..15).map { convertDrawableToBase64(drawable) }
186
+
187
+ // All results should be identical
188
+ val firstResult = results.first()
189
+ results.forEach { result ->
190
+ assertEquals("All conversions should be identical", firstResult, result)
191
+ }
192
+ }
193
+
194
+ @Test
195
+ fun `PROPERTY - different sizes with same color produce different base64`() =
196
+ runTest {
197
+ val color = Color.rgb(100, 150, 200)
198
+
199
+ val sizes =
200
+ listOf(
201
+ Pair(50, 50),
202
+ Pair(75, 75),
203
+ Pair(100, 100),
204
+ Pair(125, 125),
205
+ )
206
+
207
+ val results =
208
+ sizes.map { (width, height) ->
209
+ val drawable =
210
+ ColorDrawable(color).apply {
211
+ setBounds(0, 0, width, height)
212
+ }
213
+ convertDrawableToBase64(drawable)
214
+ }
215
+
216
+ // All results should be different (different sizes → different images)
217
+ val uniqueResults = results.toSet()
218
+ assertEquals(
219
+ "Different sizes should produce different base64 even with same color",
220
+ results.size,
221
+ uniqueResults.size,
222
+ )
223
+ }
224
+ }
@@ -0,0 +1,146 @@
1
+ package com.reactnativestripesdk
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Color
5
+ import android.graphics.drawable.ColorDrawable
6
+ import kotlinx.coroutines.test.runTest
7
+ import org.junit.Assert.assertEquals
8
+ import org.junit.Assert.assertNotNull
9
+ import org.junit.Assert.assertNull
10
+ import org.junit.Assert.assertTrue
11
+ import org.junit.Test
12
+ import org.junit.runner.RunWith
13
+ import org.robolectric.RobolectricTestRunner
14
+
15
+ @RunWith(RobolectricTestRunner::class)
16
+ class DrawableConversionTest {
17
+ // ============================================
18
+ // convertDrawableToBase64 Tests
19
+ // ============================================
20
+
21
+ @Test
22
+ fun `convertDrawableToBase64 returns non-null for valid drawable`() =
23
+ runTest {
24
+ val drawable =
25
+ ColorDrawable(Color.RED).apply {
26
+ setBounds(0, 0, 100, 100)
27
+ }
28
+
29
+ val result = convertDrawableToBase64(drawable)
30
+
31
+ assertNotNull("Base64 should not be null for valid drawable", result)
32
+ assertTrue("Base64 should not be empty", result!!.isNotEmpty())
33
+ }
34
+
35
+ @Test
36
+ fun `convertDrawableToBase64 returns consistent results for same drawable`() =
37
+ runTest {
38
+ val drawable =
39
+ ColorDrawable(Color.BLUE).apply {
40
+ setBounds(0, 0, 100, 100)
41
+ }
42
+
43
+ val result1 = convertDrawableToBase64(drawable)
44
+ val result2 = convertDrawableToBase64(drawable)
45
+ val result3 = convertDrawableToBase64(drawable)
46
+
47
+ assertEquals("Multiple calls should return identical base64", result1, result2)
48
+ assertEquals("Multiple calls should return identical base64", result2, result3)
49
+ }
50
+
51
+ @Test
52
+ fun `convertDrawableToBase64 returns null for invalid drawable`() =
53
+ runTest {
54
+ // Drawable with 0 intrinsic size (never set bounds)
55
+ val drawable = ColorDrawable(Color.RED)
56
+
57
+ val result = convertDrawableToBase64(drawable)
58
+
59
+ assertNull("Base64 should be null for invalid drawable", result)
60
+ }
61
+
62
+ @Test
63
+ fun `convertDrawableToBase64 result is valid base64 format`() =
64
+ runTest {
65
+ val drawable =
66
+ ColorDrawable(Color.GREEN).apply {
67
+ setBounds(0, 0, 50, 50)
68
+ }
69
+
70
+ val result = convertDrawableToBase64(drawable)
71
+
72
+ assertNotNull(result)
73
+ // Valid base64 should match pattern: [A-Za-z0-9+/=]+
74
+ assertTrue(
75
+ "Result should be valid base64",
76
+ result!!.matches(Regex("^[A-Za-z0-9+/=\\n]+$")),
77
+ )
78
+ }
79
+
80
+ @Test
81
+ fun `convertDrawableToBase64 result size is reasonable`() =
82
+ runTest {
83
+ val drawable =
84
+ ColorDrawable(Color.YELLOW).apply {
85
+ setBounds(0, 0, 100, 100)
86
+ }
87
+
88
+ val result = convertDrawableToBase64(drawable)
89
+
90
+ assertNotNull(result)
91
+ // Should be larger than tiny placeholder (134 bytes)
92
+ // but smaller than unreasonably large
93
+ assertTrue(
94
+ "Base64 should be larger than 200 chars (not a 1x1 placeholder)",
95
+ result!!.length > 200,
96
+ )
97
+ assertTrue(
98
+ "Base64 should be smaller than 100KB",
99
+ result.length < 100_000,
100
+ )
101
+ }
102
+
103
+ // ============================================
104
+ // getBitmapFromDrawable Tests
105
+ // ============================================
106
+
107
+ @Test
108
+ fun `getBitmapFromDrawable returns correct size bitmap`() {
109
+ val drawable =
110
+ ColorDrawable(Color.CYAN).apply {
111
+ setBounds(0, 0, 150, 100)
112
+ }
113
+
114
+ val bitmap = getBitmapFromDrawable(drawable)
115
+
116
+ assertNotNull("Bitmap should not be null", bitmap)
117
+ assertEquals("Bitmap width should match drawable", 150, bitmap!!.width)
118
+ assertEquals("Bitmap height should match drawable", 100, bitmap.height)
119
+ assertEquals("Bitmap should use ARGB_8888", Bitmap.Config.ARGB_8888, bitmap.config)
120
+ }
121
+
122
+ @Test
123
+ fun `getBitmapFromDrawable returns null for zero-size drawable`() {
124
+ val drawable = ColorDrawable(Color.MAGENTA)
125
+ // Don't set bounds, intrinsic size will be -1
126
+
127
+ val bitmap = getBitmapFromDrawable(drawable)
128
+
129
+ assertNull("Bitmap should be null for invalid drawable", bitmap)
130
+ }
131
+
132
+ @Test
133
+ fun `getBitmapFromDrawable preserves drawable dimensions`() {
134
+ // Test with typical card icon dimensions
135
+ val drawable =
136
+ ColorDrawable(Color.RED).apply {
137
+ setBounds(0, 0, 147, 105)
138
+ }
139
+
140
+ val bitmap = getBitmapFromDrawable(drawable)
141
+
142
+ assertNotNull(bitmap)
143
+ assertEquals("Width should be preserved", 147, bitmap!!.width)
144
+ assertEquals("Height should be preserved", 105, bitmap.height)
145
+ }
146
+ }
@@ -0,0 +1,150 @@
1
+ package com.reactnativestripesdk
2
+
3
+ import android.graphics.Canvas
4
+ import android.graphics.Color
5
+ import android.graphics.ColorFilter
6
+ import android.graphics.PixelFormat
7
+ import android.graphics.drawable.Drawable
8
+ import kotlinx.coroutines.delay
9
+ import kotlinx.coroutines.launch
10
+ import kotlinx.coroutines.test.runTest
11
+ import org.junit.Assert.assertNotNull
12
+ import org.junit.Assert.assertTrue
13
+ import org.junit.Test
14
+ import org.junit.runner.RunWith
15
+ import org.robolectric.RobolectricTestRunner
16
+
17
+ @RunWith(RobolectricTestRunner::class)
18
+ class DrawableLoadingTest {
19
+ /**
20
+ * Mock drawable that simulates DelegateDrawable's async loading behavior
21
+ */
22
+ class MockAsyncDrawable : Drawable() {
23
+ private var loaded = false
24
+ private var width = 1
25
+ private var height = 1
26
+
27
+ override fun getIntrinsicWidth(): Int = width
28
+
29
+ override fun getIntrinsicHeight(): Int = height
30
+
31
+ suspend fun simulateLoading(delayMs: Long = 100) {
32
+ delay(delayMs)
33
+ width = 150
34
+ height = 100
35
+ loaded = true
36
+ invalidateSelf() // Trigger callback
37
+ }
38
+
39
+ override fun draw(canvas: Canvas) {
40
+ // Draw a simple colored rectangle
41
+ canvas.drawColor(Color.RED)
42
+ }
43
+
44
+ override fun setAlpha(alpha: Int) {}
45
+
46
+ override fun setColorFilter(colorFilter: ColorFilter?) {}
47
+
48
+ override fun getOpacity(): Int = PixelFormat.OPAQUE
49
+ }
50
+
51
+ @Test
52
+ fun `waitForDrawableToLoad returns immediately for already loaded drawable`() =
53
+ runTest {
54
+ val drawable = MockAsyncDrawable()
55
+ // Pre-load the drawable
56
+ drawable.simulateLoading(0)
57
+
58
+ val startTime = System.currentTimeMillis()
59
+ val result = waitForDrawableToLoad(drawable, timeoutMs = 3000)
60
+ val elapsed = System.currentTimeMillis() - startTime
61
+
62
+ assertNotNull(result)
63
+ assertTrue("Should return quickly (< 100ms)", elapsed < 100)
64
+ assertTrue("Drawable should be loaded", result.intrinsicWidth > 1)
65
+ }
66
+
67
+ @Test
68
+ fun `waitForDrawableToLoad waits for drawable to load`() =
69
+ runTest {
70
+ val drawable = MockAsyncDrawable()
71
+
72
+ // Start async loading
73
+ launch {
74
+ drawable.simulateLoading(100)
75
+ }
76
+
77
+ val result = waitForDrawableToLoad(drawable, timeoutMs = 3000)
78
+
79
+ assertNotNull(result)
80
+ assertTrue("Drawable should be loaded after waiting", result.intrinsicWidth > 1)
81
+ assertTrue("Drawable should be loaded after waiting", result.intrinsicHeight > 1)
82
+ assertTrue("Width should be 150", result.intrinsicWidth == 150)
83
+ assertTrue("Height should be 100", result.intrinsicHeight == 100)
84
+ }
85
+
86
+ @Test
87
+ fun `waitForDrawableToLoad times out gracefully for drawable that never loads`() =
88
+ runTest {
89
+ val drawable = MockAsyncDrawable() // Never call simulateLoading()
90
+
91
+ val result = waitForDrawableToLoad(drawable, timeoutMs = 500)
92
+
93
+ // Should return the drawable even if timeout (best effort)
94
+ assertNotNull(result)
95
+ }
96
+
97
+ @Test
98
+ fun `convertDrawableToBase64 with async drawable returns consistent results`() =
99
+ runTest {
100
+ val drawable1 = MockAsyncDrawable()
101
+ val drawable2 = MockAsyncDrawable()
102
+
103
+ // Simulate both loading
104
+ launch { drawable1.simulateLoading(50) }
105
+ launch { drawable2.simulateLoading(100) }
106
+
107
+ val result1 = convertDrawableToBase64(drawable1)
108
+ val result2 = convertDrawableToBase64(drawable2)
109
+
110
+ assertNotNull("Result1 should not be null", result1)
111
+ assertNotNull("Result2 should not be null", result2)
112
+
113
+ // Both should be non-empty and similar length (same dimensions)
114
+ assertTrue("Both results should be non-empty", result1!!.isNotEmpty())
115
+ assertTrue("Both results should be non-empty", result2!!.isNotEmpty())
116
+
117
+ // They should be similar length since both are 150x100
118
+ val lengthDiff = kotlin.math.abs(result1.length - result2.length)
119
+ assertTrue(
120
+ "Results should have similar length (both 150x100), diff: $lengthDiff",
121
+ lengthDiff < result1.length * 0.1, // Within 10%
122
+ )
123
+ }
124
+
125
+ @Test
126
+ fun `waitForDrawableToLoad handles multiple concurrent calls`() =
127
+ runTest {
128
+ val drawable1 = MockAsyncDrawable()
129
+ val drawable2 = MockAsyncDrawable()
130
+ val drawable3 = MockAsyncDrawable()
131
+
132
+ // Start all loading at the same time
133
+ launch { drawable1.simulateLoading(50) }
134
+ launch { drawable2.simulateLoading(75) }
135
+ launch { drawable3.simulateLoading(100) }
136
+
137
+ // Wait for all concurrently
138
+ val result1 = waitForDrawableToLoad(drawable1, timeoutMs = 3000)
139
+ val result2 = waitForDrawableToLoad(drawable2, timeoutMs = 3000)
140
+ val result3 = waitForDrawableToLoad(drawable3, timeoutMs = 3000)
141
+
142
+ assertNotNull(result1)
143
+ assertNotNull(result2)
144
+ assertNotNull(result3)
145
+
146
+ assertTrue("All drawables should be loaded", result1.intrinsicWidth > 1)
147
+ assertTrue("All drawables should be loaded", result2.intrinsicWidth > 1)
148
+ assertTrue("All drawables should be loaded", result3.intrinsicWidth > 1)
149
+ }
150
+ }
@@ -0,0 +1,186 @@
1
+ package com.reactnativestripesdk
2
+
3
+ import android.graphics.Color
4
+ import android.graphics.drawable.ColorDrawable
5
+ import kotlinx.coroutines.test.runTest
6
+ import org.junit.Assert.assertEquals
7
+ import org.junit.Assert.assertNotEquals
8
+ import org.junit.Assert.assertNotNull
9
+ import org.junit.Assert.assertTrue
10
+ import org.junit.Test
11
+ import org.junit.runner.RunWith
12
+ import org.robolectric.RobolectricTestRunner
13
+
14
+ /**
15
+ * Regression tests for the payment option image consistency bug.
16
+ *
17
+ * Issue: PaymentOption.icon() was returning a DelegateDrawable that loaded asynchronously,
18
+ * causing inconsistent base64 strings across multiple calls. Sometimes it returned a 1x1
19
+ * placeholder (134 bytes), sometimes the full image (5650 bytes).
20
+ *
21
+ * These tests ensure this bug never returns.
22
+ */
23
+ @RunWith(RobolectricTestRunner::class)
24
+ class PaymentOptionImageConsistencyTest {
25
+ @Test
26
+ fun `REGRESSION - multiple calls return consistent base64 strings`() =
27
+ runTest {
28
+ // This test specifically guards against the original bug where
29
+ // multiple calls to icon() returned inconsistent results
30
+ val drawable =
31
+ ColorDrawable(Color.BLUE).apply {
32
+ setBounds(0, 0, 147, 105) // Typical card icon size
33
+ }
34
+
35
+ val results = mutableListOf<String?>()
36
+ repeat(10) {
37
+ results.add(convertDrawableToBase64(drawable))
38
+ }
39
+
40
+ // All results should be identical
41
+ val uniqueResults = results.toSet()
42
+ assertEquals(
43
+ "All 10 calls should return identical base64 (was returning different values due to async loading bug)",
44
+ 1,
45
+ uniqueResults.size,
46
+ )
47
+
48
+ // Verify the result is not null
49
+ assertNotNull("Base64 result should not be null", results.first())
50
+ }
51
+
52
+ @Test
53
+ fun `REGRESSION - base64 is not 1x1 placeholder`() =
54
+ runTest {
55
+ // This test guards against returning the 1x1 placeholder image
56
+ // Original bug: Sometimes returned 134-byte 1x1 black placeholder
57
+ val drawable =
58
+ ColorDrawable(Color.RED).apply {
59
+ setBounds(0, 0, 147, 105)
60
+ }
61
+
62
+ val result = convertDrawableToBase64(drawable)
63
+
64
+ assertNotNull(result)
65
+ // Original bug: 1x1 placeholder was 134 bytes
66
+ // Real image should be much larger
67
+ assertTrue(
68
+ "Base64 should be > 200 chars (original bug returned 134-byte 1x1 placeholder)",
69
+ result!!.length > 200,
70
+ )
71
+ }
72
+
73
+ @Test
74
+ fun `REGRESSION - bitmap has correct dimensions not 1x1`() =
75
+ runTest {
76
+ val drawable =
77
+ ColorDrawable(Color.GREEN).apply {
78
+ setBounds(0, 0, 147, 105)
79
+ }
80
+
81
+ val bitmap = getBitmapFromDrawable(drawable)
82
+
83
+ assertNotNull(bitmap)
84
+ assertNotEquals(
85
+ "Bitmap width should not be 1 (original bug captured 1x1 placeholder)",
86
+ 1,
87
+ bitmap!!.width,
88
+ )
89
+ assertNotEquals(
90
+ "Bitmap height should not be 1 (original bug captured 1x1 placeholder)",
91
+ 1,
92
+ bitmap.height,
93
+ )
94
+ assertEquals("Bitmap width should match drawable", 147, bitmap.width)
95
+ assertEquals("Bitmap height should match drawable", 105, bitmap.height)
96
+ }
97
+
98
+ @Test
99
+ fun `REGRESSION - rapid successive calls all return full image`() =
100
+ runTest {
101
+ // Simulate rapid successive calls like what happens when
102
+ // retrievePaymentOptionSelection() is called multiple times quickly
103
+ val drawable =
104
+ ColorDrawable(Color.MAGENTA).apply {
105
+ setBounds(0, 0, 147, 105)
106
+ }
107
+
108
+ val results = mutableListOf<String?>()
109
+ // Rapid fire - no delays
110
+ repeat(20) {
111
+ results.add(convertDrawableToBase64(drawable))
112
+ }
113
+
114
+ // All should be non-null
115
+ assertTrue(
116
+ "All results should be non-null",
117
+ results.all { it != null },
118
+ )
119
+
120
+ // All should be full images, not placeholders
121
+ assertTrue(
122
+ "All results should be > 200 chars (not 1x1 placeholders)",
123
+ results.all { it!!.length > 200 },
124
+ )
125
+
126
+ // All should be identical
127
+ val uniqueResults = results.toSet()
128
+ assertEquals(
129
+ "All 20 rapid calls should return identical base64",
130
+ 1,
131
+ uniqueResults.size,
132
+ )
133
+ }
134
+
135
+ @Test
136
+ fun `REGRESSION - consistent results across different payment method types`() =
137
+ runTest {
138
+ // Test with different typical payment method icon sizes
139
+ val visaDrawable =
140
+ ColorDrawable(Color.BLUE).apply {
141
+ setBounds(0, 0, 147, 105) // Visa
142
+ }
143
+ val mastercardDrawable =
144
+ ColorDrawable(Color.RED).apply {
145
+ setBounds(0, 0, 147, 105) // Mastercard (same size)
146
+ }
147
+ val amexDrawable =
148
+ ColorDrawable(Color.CYAN).apply {
149
+ setBounds(0, 0, 147, 105) // Amex (same size)
150
+ }
151
+
152
+ val visa1 = convertDrawableToBase64(visaDrawable)
153
+ val visa2 = convertDrawableToBase64(visaDrawable)
154
+
155
+ val mc1 = convertDrawableToBase64(mastercardDrawable)
156
+ val mc2 = convertDrawableToBase64(mastercardDrawable)
157
+
158
+ val amex1 = convertDrawableToBase64(amexDrawable)
159
+ val amex2 = convertDrawableToBase64(amexDrawable)
160
+
161
+ // Each card type should return consistent results
162
+ assertEquals("Visa should be consistent", visa1, visa2)
163
+ assertEquals("Mastercard should be consistent", mc1, mc2)
164
+ assertEquals("Amex should be consistent", amex1, amex2)
165
+
166
+ // Different card types should have different images
167
+ assertNotEquals("Visa and Mastercard should differ", visa1, mc1)
168
+ assertNotEquals("Mastercard and Amex should differ", mc1, amex1)
169
+ }
170
+
171
+ @Test
172
+ fun `REGRESSION - setBounds is applied before drawing`() {
173
+ // This test ensures the original fix (setBounds) is still working
174
+ val drawable =
175
+ ColorDrawable(Color.YELLOW).apply {
176
+ setBounds(0, 0, 100, 80)
177
+ }
178
+
179
+ val bitmap = getBitmapFromDrawable(drawable)
180
+
181
+ assertNotNull(bitmap)
182
+ // The bitmap dimensions should match the bounds we set
183
+ assertEquals("Bitmap should respect setBounds width", 100, bitmap!!.width)
184
+ assertEquals("Bitmap should respect setBounds height", 80, bitmap.height)
185
+ }
186
+ }