@stripe/stripe-react-native 0.57.2 → 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.
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/reactnativestripesdk/PaymentSheetManager.kt +131 -30
- package/android/src/main/java/com/reactnativestripesdk/customersheet/CustomerSheetManager.kt +38 -13
- package/android/src/test/java/com/reactnativestripesdk/DrawableConversionPropertyTest.kt +224 -0
- package/android/src/test/java/com/reactnativestripesdk/DrawableConversionTest.kt +146 -0
- package/android/src/test/java/com/reactnativestripesdk/DrawableLoadingTest.kt +150 -0
- package/android/src/test/java/com/reactnativestripesdk/PaymentOptionImageConsistencyTest.kt +186 -0
- package/lib/commonjs/components/AddToWalletButton.js +1 -1
- package/lib/commonjs/components/AddToWalletButton.js.map +1 -1
- package/lib/commonjs/components/AddressSheet.js +1 -1
- package/lib/commonjs/components/AddressSheet.js.map +1 -1
- package/lib/commonjs/components/AuBECSDebitForm.js +1 -1
- package/lib/commonjs/components/AuBECSDebitForm.js.map +1 -1
- package/lib/commonjs/components/CardField.js +1 -1
- package/lib/commonjs/components/CardField.js.map +1 -1
- package/lib/commonjs/components/CardForm.js +1 -1
- package/lib/commonjs/components/CardForm.js.map +1 -1
- package/lib/commonjs/components/PlatformPayButton.js +1 -1
- package/lib/commonjs/components/PlatformPayButton.js.map +1 -1
- package/lib/commonjs/components/StripeContainer.js +1 -1
- package/lib/commonjs/components/StripeContainer.js.map +1 -1
- package/lib/commonjs/connect/Components.js +1 -1
- package/lib/commonjs/connect/Components.js.map +1 -1
- package/lib/commonjs/connect/ConnectComponentsProvider.js +1 -1
- package/lib/commonjs/connect/ConnectComponentsProvider.js.map +1 -1
- package/lib/commonjs/connect/EmbeddedComponent.js +1 -1
- package/lib/commonjs/connect/EmbeddedComponent.js.map +1 -1
- package/lib/commonjs/connect/ModalCloseButton.js +1 -1
- package/lib/commonjs/connect/ModalCloseButton.js.map +1 -1
- package/lib/commonjs/connect/NavigationBar.js +1 -1
- package/lib/commonjs/connect/NavigationBar.js.map +1 -1
- package/lib/commonjs/helpers.js +1 -1
- package/lib/commonjs/hooks/useOnramp.js +1 -1
- package/lib/commonjs/hooks/useOnramp.js.map +1 -1
- package/lib/commonjs/specs/NativeAddToWalletButton.js +1 -1
- package/lib/commonjs/specs/NativeAddressSheet.js +1 -1
- package/lib/commonjs/specs/NativeApplePayButton.js +1 -1
- package/lib/commonjs/specs/NativeAuBECSDebitForm.js +1 -1
- package/lib/commonjs/specs/NativeCardField.js +1 -1
- package/lib/commonjs/specs/NativeCardField.js.map +1 -1
- package/lib/commonjs/specs/NativeCardForm.js +1 -1
- package/lib/commonjs/specs/NativeCardForm.js.map +1 -1
- package/lib/commonjs/specs/NativeConnectAccountOnboardingView.js +1 -1
- package/lib/commonjs/specs/NativeEmbeddedPaymentElement.js +1 -1
- package/lib/commonjs/specs/NativeEmbeddedPaymentElement.js.map +1 -1
- package/lib/commonjs/specs/NativeGooglePayButton.js +1 -1
- package/lib/commonjs/specs/NativeNavigationBar.js +1 -1
- package/lib/commonjs/specs/NativeStripeContainer.js +1 -1
- package/lib/commonjs/types/EmbeddedPaymentElement.js +1 -1
- package/lib/commonjs/types/EmbeddedPaymentElement.js.map +1 -1
- package/lib/module/components/AddToWalletButton.js +1 -1
- package/lib/module/components/AddToWalletButton.js.map +1 -1
- package/lib/module/components/AddressSheet.js +1 -1
- package/lib/module/components/AddressSheet.js.map +1 -1
- package/lib/module/components/AuBECSDebitForm.js +1 -1
- package/lib/module/components/AuBECSDebitForm.js.map +1 -1
- package/lib/module/components/CardField.js +1 -1
- package/lib/module/components/CardField.js.map +1 -1
- package/lib/module/components/CardForm.js +1 -1
- package/lib/module/components/CardForm.js.map +1 -1
- package/lib/module/components/PlatformPayButton.js +1 -1
- package/lib/module/components/PlatformPayButton.js.map +1 -1
- package/lib/module/components/StripeContainer.js +1 -1
- package/lib/module/components/StripeContainer.js.map +1 -1
- package/lib/module/connect/Components.js +1 -1
- package/lib/module/connect/Components.js.map +1 -1
- package/lib/module/connect/ConnectComponentsProvider.js +1 -1
- package/lib/module/connect/ConnectComponentsProvider.js.map +1 -1
- package/lib/module/connect/EmbeddedComponent.js +1 -1
- package/lib/module/connect/EmbeddedComponent.js.map +1 -1
- package/lib/module/connect/ModalCloseButton.js +1 -1
- package/lib/module/connect/ModalCloseButton.js.map +1 -1
- package/lib/module/connect/NavigationBar.js +1 -1
- package/lib/module/connect/NavigationBar.js.map +1 -1
- package/lib/module/helpers.js +1 -1
- package/lib/module/hooks/useOnramp.js +1 -1
- package/lib/module/hooks/useOnramp.js.map +1 -1
- package/lib/module/specs/NativeAddToWalletButton.js +1 -1
- package/lib/module/specs/NativeAddressSheet.js +1 -1
- package/lib/module/specs/NativeApplePayButton.js +1 -1
- package/lib/module/specs/NativeAuBECSDebitForm.js +1 -1
- package/lib/module/specs/NativeCardField.js +1 -1
- package/lib/module/specs/NativeCardField.js.map +1 -1
- package/lib/module/specs/NativeCardForm.js +1 -1
- package/lib/module/specs/NativeCardForm.js.map +1 -1
- package/lib/module/specs/NativeConnectAccountOnboardingView.js +1 -1
- package/lib/module/specs/NativeEmbeddedPaymentElement.js +1 -1
- package/lib/module/specs/NativeEmbeddedPaymentElement.js.map +1 -1
- package/lib/module/specs/NativeGooglePayButton.js +1 -1
- package/lib/module/specs/NativeNavigationBar.js +1 -1
- package/lib/module/specs/NativeStripeContainer.js +1 -1
- package/lib/module/types/EmbeddedPaymentElement.js +1 -1
- package/lib/module/types/EmbeddedPaymentElement.js.map +1 -1
- package/lib/typescript/src/connect/EmbeddedComponent.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useOnramp.d.ts +2 -1
- package/lib/typescript/src/hooks/useOnramp.d.ts.map +1 -1
- package/package.json +12 -5
- package/src/connect/EmbeddedComponent.tsx +10 -10
- package/src/hooks/useOnramp.tsx +5 -1
- package/android/.idea/AndroidProjectSystem.xml +0 -6
- package/android/.idea/caches/deviceStreaming.xml +0 -1029
- package/android/.idea/compiler.xml +0 -6
- package/android/.idea/gradle.xml +0 -19
- package/android/.idea/migrations.xml +0 -10
- package/android/.idea/misc.xml +0 -10
- package/android/.idea/runConfigurations.xml +0 -17
- package/android/.idea/vcs.xml +0 -6
- package/android/local.properties +0 -8
- package/ios/StripeSdk.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/ios/StripeSdk.xcodeproj/project.xcworkspace/xcuserdata/tianzhao.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/StripeSdk.xcodeproj/xcuserdata/tianzhao.xcuserdatad/xcschemes/xcschememanagement.plist +0 -19
package/android/build.gradle
CHANGED
|
@@ -183,6 +183,7 @@ dependencies {
|
|
|
183
183
|
testImplementation "org.mockito:mockito-core:3.+"
|
|
184
184
|
testImplementation "org.robolectric:robolectric:4.10"
|
|
185
185
|
testImplementation "androidx.test:core:1.4.0"
|
|
186
|
+
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0"
|
|
186
187
|
|
|
187
188
|
implementation "androidx.compose.ui:ui:1.7.8"
|
|
188
189
|
implementation "androidx.compose.foundation:foundation-layout:1.7.8"
|
|
@@ -58,8 +58,11 @@ import kotlinx.coroutines.CoroutineScope
|
|
|
58
58
|
import kotlinx.coroutines.Dispatchers
|
|
59
59
|
import kotlinx.coroutines.delay
|
|
60
60
|
import kotlinx.coroutines.launch
|
|
61
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
62
|
+
import kotlinx.coroutines.withTimeoutOrNull
|
|
61
63
|
import java.io.ByteArrayOutputStream
|
|
62
64
|
import kotlin.Exception
|
|
65
|
+
import kotlin.coroutines.resume
|
|
63
66
|
|
|
64
67
|
@OptIn(
|
|
65
68
|
ReactNativeSdkInternal::class,
|
|
@@ -140,28 +143,42 @@ class PaymentSheetManager(
|
|
|
140
143
|
|
|
141
144
|
val paymentOptionCallback =
|
|
142
145
|
PaymentOptionResultCallback { paymentOptionResult ->
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
val imageString =
|
|
146
|
+
paymentOptionResult.paymentOption?.let { paymentOption ->
|
|
147
|
+
// Convert drawable to bitmap asynchronously to avoid shared state issues
|
|
148
|
+
CoroutineScope(Dispatchers.Default).launch {
|
|
149
|
+
val imageString =
|
|
150
|
+
try {
|
|
151
|
+
convertDrawableToBase64(paymentOption.icon())
|
|
152
|
+
} catch (e: Exception) {
|
|
153
|
+
val result =
|
|
154
|
+
createError(
|
|
155
|
+
PaymentSheetErrorType.Failed.toString(),
|
|
156
|
+
"Failed to process payment option image: ${e.message}",
|
|
157
|
+
)
|
|
158
|
+
resolvePresentPromise(result)
|
|
159
|
+
return@launch
|
|
160
|
+
}
|
|
161
|
+
|
|
147
162
|
val option: WritableMap = Arguments.createMap()
|
|
148
|
-
option.putString("label",
|
|
163
|
+
option.putString("label", paymentOption.label)
|
|
149
164
|
option.putString("image", imageString)
|
|
150
165
|
val additionalFields: Map<String, Any> = mapOf("didCancel" to paymentOptionResult.didCancel)
|
|
151
|
-
createResult("paymentOption", option, additionalFields)
|
|
166
|
+
val result = createResult("paymentOption", option, additionalFields)
|
|
167
|
+
resolvePresentPromise(result)
|
|
152
168
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
169
|
+
} ?: run {
|
|
170
|
+
val result =
|
|
171
|
+
if (paymentSheetTimedOut) {
|
|
172
|
+
paymentSheetTimedOut = false
|
|
173
|
+
createError(PaymentSheetErrorType.Timeout.toString(), "The payment has timed out")
|
|
174
|
+
} else {
|
|
175
|
+
createError(
|
|
176
|
+
PaymentSheetErrorType.Canceled.toString(),
|
|
177
|
+
"The payment option selection flow has been canceled",
|
|
178
|
+
)
|
|
163
179
|
}
|
|
164
|
-
|
|
180
|
+
resolvePresentPromise(result)
|
|
181
|
+
}
|
|
165
182
|
}
|
|
166
183
|
|
|
167
184
|
val paymentResultCallback =
|
|
@@ -413,16 +430,31 @@ class PaymentSheetManager(
|
|
|
413
430
|
private fun configureFlowController() {
|
|
414
431
|
val onFlowControllerConfigure =
|
|
415
432
|
PaymentSheet.FlowController.ConfigCallback { _, _ ->
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
val imageString =
|
|
433
|
+
flowController?.getPaymentOption()?.let { paymentOption ->
|
|
434
|
+
// Launch async job to convert drawable, but resolve promise synchronously
|
|
435
|
+
CoroutineScope(Dispatchers.Default).launch {
|
|
436
|
+
val imageString =
|
|
437
|
+
try {
|
|
438
|
+
convertDrawableToBase64(paymentOption.icon())
|
|
439
|
+
} catch (e: Exception) {
|
|
440
|
+
val result =
|
|
441
|
+
createError(
|
|
442
|
+
PaymentSheetErrorType.Failed.toString(),
|
|
443
|
+
"Failed to process payment option image: ${e.message}",
|
|
444
|
+
)
|
|
445
|
+
initPromise.resolve(result)
|
|
446
|
+
return@launch
|
|
447
|
+
}
|
|
448
|
+
|
|
420
449
|
val option: WritableMap = Arguments.createMap()
|
|
421
|
-
option.putString("label",
|
|
450
|
+
option.putString("label", paymentOption.label)
|
|
422
451
|
option.putString("image", imageString)
|
|
423
|
-
createResult("paymentOption", option)
|
|
424
|
-
|
|
425
|
-
|
|
452
|
+
val result = createResult("paymentOption", option)
|
|
453
|
+
initPromise.resolve(result)
|
|
454
|
+
}
|
|
455
|
+
} ?: run {
|
|
456
|
+
initPromise.resolve(Arguments.createMap())
|
|
457
|
+
}
|
|
426
458
|
}
|
|
427
459
|
|
|
428
460
|
if (!paymentIntentClientSecret.isNullOrEmpty()) {
|
|
@@ -550,17 +582,86 @@ class PaymentSheetManager(
|
|
|
550
582
|
}
|
|
551
583
|
}
|
|
552
584
|
|
|
585
|
+
suspend fun waitForDrawableToLoad(
|
|
586
|
+
drawable: Drawable,
|
|
587
|
+
timeoutMs: Long = 3000,
|
|
588
|
+
): Drawable {
|
|
589
|
+
// If already loaded, return immediately
|
|
590
|
+
if (drawable.intrinsicWidth > 1 && drawable.intrinsicHeight > 1) {
|
|
591
|
+
return drawable
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Use callback to be notified when drawable finishes loading
|
|
595
|
+
return withTimeoutOrNull(timeoutMs) {
|
|
596
|
+
suspendCancellableCoroutine { continuation ->
|
|
597
|
+
val callback =
|
|
598
|
+
object : Drawable.Callback {
|
|
599
|
+
override fun invalidateDrawable(who: Drawable) {
|
|
600
|
+
// Drawable has changed/loaded - check if it's ready now
|
|
601
|
+
if (who.intrinsicWidth > 1 && who.intrinsicHeight > 1) {
|
|
602
|
+
who.callback = null // Remove callback
|
|
603
|
+
if (continuation.isActive) {
|
|
604
|
+
continuation.resume(who)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
override fun scheduleDrawable(
|
|
610
|
+
who: Drawable,
|
|
611
|
+
what: Runnable,
|
|
612
|
+
`when`: Long,
|
|
613
|
+
) {}
|
|
614
|
+
|
|
615
|
+
override fun unscheduleDrawable(
|
|
616
|
+
who: Drawable,
|
|
617
|
+
what: Runnable,
|
|
618
|
+
) {}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
drawable.callback = callback
|
|
622
|
+
|
|
623
|
+
// Trigger an invalidation to check if it loads immediately
|
|
624
|
+
drawable.invalidateSelf()
|
|
625
|
+
|
|
626
|
+
continuation.invokeOnCancellation { drawable.callback = null }
|
|
627
|
+
}
|
|
628
|
+
} ?: drawable // Return drawable even if timeout (best effort)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
suspend fun convertDrawableToBase64(drawable: Drawable): String? {
|
|
632
|
+
val loadedDrawable = waitForDrawableToLoad(drawable)
|
|
633
|
+
val bitmap = getBitmapFromDrawable(loadedDrawable)
|
|
634
|
+
return getBase64FromBitmap(bitmap)
|
|
635
|
+
}
|
|
636
|
+
|
|
553
637
|
fun getBitmapFromDrawable(drawable: Drawable): Bitmap? {
|
|
554
638
|
val drawableCompat = DrawableCompat.wrap(drawable).mutate()
|
|
555
|
-
|
|
639
|
+
|
|
640
|
+
// Determine the size to use - prefer intrinsic size, fall back to bounds
|
|
641
|
+
val width =
|
|
642
|
+
if (drawableCompat.intrinsicWidth > 0) {
|
|
643
|
+
drawableCompat.intrinsicWidth
|
|
644
|
+
} else {
|
|
645
|
+
drawableCompat.bounds.width()
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
val height =
|
|
649
|
+
if (drawableCompat.intrinsicHeight > 0) {
|
|
650
|
+
drawableCompat.intrinsicHeight
|
|
651
|
+
} else {
|
|
652
|
+
drawableCompat.bounds.height()
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (width <= 0 || height <= 0) {
|
|
556
656
|
return null
|
|
557
657
|
}
|
|
558
|
-
|
|
559
|
-
|
|
658
|
+
|
|
659
|
+
val bitmap = createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
560
660
|
bitmap.eraseColor(Color.TRANSPARENT)
|
|
561
661
|
val canvas = Canvas(bitmap)
|
|
562
|
-
|
|
563
|
-
|
|
662
|
+
drawableCompat.setBounds(0, 0, canvas.width, canvas.height)
|
|
663
|
+
drawableCompat.draw(canvas)
|
|
664
|
+
|
|
564
665
|
return bitmap
|
|
565
666
|
}
|
|
566
667
|
|
package/android/src/main/java/com/reactnativestripesdk/customersheet/CustomerSheetManager.kt
CHANGED
|
@@ -17,8 +17,7 @@ import com.reactnativestripesdk.ReactNativeCustomerSessionProvider
|
|
|
17
17
|
import com.reactnativestripesdk.buildBillingDetails
|
|
18
18
|
import com.reactnativestripesdk.buildBillingDetailsCollectionConfiguration
|
|
19
19
|
import com.reactnativestripesdk.buildPaymentSheetAppearance
|
|
20
|
-
import com.reactnativestripesdk.
|
|
21
|
-
import com.reactnativestripesdk.getBitmapFromDrawable
|
|
20
|
+
import com.reactnativestripesdk.convertDrawableToBase64
|
|
22
21
|
import com.reactnativestripesdk.mapToCardBrandAcceptance
|
|
23
22
|
import com.reactnativestripesdk.utils.CreateTokenErrorType
|
|
24
23
|
import com.reactnativestripesdk.utils.ErrorType
|
|
@@ -163,25 +162,49 @@ class CustomerSheetManager(
|
|
|
163
162
|
}
|
|
164
163
|
|
|
165
164
|
private fun handleResult(result: CustomerSheetResult) {
|
|
166
|
-
var promiseResult = Arguments.createMap()
|
|
167
165
|
when (result) {
|
|
168
166
|
is CustomerSheetResult.Failed -> {
|
|
169
167
|
resolvePresentPromise(createError(ErrorType.Failed.toString(), result.exception))
|
|
170
168
|
}
|
|
171
169
|
|
|
172
170
|
is CustomerSheetResult.Selected -> {
|
|
173
|
-
|
|
171
|
+
// Convert drawable asynchronously to avoid shared state issues
|
|
172
|
+
CoroutineScope(Dispatchers.Default).launch {
|
|
173
|
+
try {
|
|
174
|
+
val promiseResult = createPaymentOptionResult(result.selection)
|
|
175
|
+
resolvePresentPromise(promiseResult)
|
|
176
|
+
} catch (e: Exception) {
|
|
177
|
+
resolvePresentPromise(
|
|
178
|
+
createError(
|
|
179
|
+
ErrorType.Failed.toString(),
|
|
180
|
+
"Failed to process payment option image: ${e.message}",
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
174
185
|
}
|
|
175
186
|
|
|
176
187
|
is CustomerSheetResult.Canceled -> {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
188
|
+
// Convert drawable asynchronously to avoid shared state issues
|
|
189
|
+
CoroutineScope(Dispatchers.Default).launch {
|
|
190
|
+
try {
|
|
191
|
+
val promiseResult = createPaymentOptionResult(result.selection)
|
|
192
|
+
promiseResult.putMap(
|
|
193
|
+
"error",
|
|
194
|
+
Arguments.createMap().also { it.putString("code", ErrorType.Canceled.toString()) },
|
|
195
|
+
)
|
|
196
|
+
resolvePresentPromise(promiseResult)
|
|
197
|
+
} catch (e: Exception) {
|
|
198
|
+
resolvePresentPromise(
|
|
199
|
+
createError(
|
|
200
|
+
ErrorType.Failed.toString(),
|
|
201
|
+
"Failed to process payment option image: ${e.message}",
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
182
206
|
}
|
|
183
207
|
}
|
|
184
|
-
resolvePresentPromise(promiseResult)
|
|
185
208
|
}
|
|
186
209
|
|
|
187
210
|
override fun onPresent() {
|
|
@@ -355,7 +378,7 @@ class CustomerSheetManager(
|
|
|
355
378
|
)
|
|
356
379
|
}
|
|
357
380
|
|
|
358
|
-
internal fun createPaymentOptionResult(selection: PaymentOptionSelection?): WritableMap {
|
|
381
|
+
internal suspend fun createPaymentOptionResult(selection: PaymentOptionSelection?): WritableMap {
|
|
359
382
|
var paymentOptionResult = Arguments.createMap()
|
|
360
383
|
|
|
361
384
|
when (selection) {
|
|
@@ -392,16 +415,18 @@ class CustomerSheetManager(
|
|
|
392
415
|
}.build()
|
|
393
416
|
}
|
|
394
417
|
|
|
395
|
-
private fun buildResult(
|
|
418
|
+
private suspend fun buildResult(
|
|
396
419
|
label: String,
|
|
397
420
|
drawable: Drawable,
|
|
398
421
|
paymentMethod: PaymentMethod?,
|
|
399
422
|
): WritableMap {
|
|
423
|
+
val imageString = convertDrawableToBase64(drawable)
|
|
424
|
+
|
|
400
425
|
val result = Arguments.createMap()
|
|
401
426
|
val paymentOption =
|
|
402
427
|
Arguments.createMap().also {
|
|
403
428
|
it.putString("label", label)
|
|
404
|
-
it.putString("image",
|
|
429
|
+
it.putString("image", imageString)
|
|
405
430
|
}
|
|
406
431
|
result.putMap("paymentOption", paymentOption)
|
|
407
432
|
if (paymentMethod != null) {
|
|
@@ -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
|
+
}
|