@stripe/stripe-react-native 0.65.1 → 0.66.0

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 (79) hide show
  1. package/android/detekt/detekt-baseline.xml +0 -10
  2. package/android/gradle.properties +1 -1
  3. package/android/src/main/java/com/reactnativestripesdk/CardFieldView.kt +51 -57
  4. package/android/src/main/java/com/reactnativestripesdk/CurrencySelectorElementEvent.kt +26 -0
  5. package/android/src/main/java/com/reactnativestripesdk/CurrencySelectorElementView.kt +129 -16
  6. package/android/src/main/java/com/reactnativestripesdk/CurrencySelectorElementViewManager.kt +17 -0
  7. package/android/src/main/java/com/reactnativestripesdk/EventEmitterCompat.kt +4 -0
  8. package/android/src/main/java/com/reactnativestripesdk/PaymentSheetManager.kt +8 -19
  9. package/android/src/main/java/com/reactnativestripesdk/StripeAbstractComposeView.kt +1 -0
  10. package/android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt +95 -26
  11. package/android/src/main/java/com/reactnativestripesdk/StripeSdkPackage.kt +1 -0
  12. package/android/src/main/java/com/reactnativestripesdk/customersheet/CustomerSheetManager.kt +2 -16
  13. package/android/src/main/java/com/reactnativestripesdk/utils/CheckoutMappers.kt +59 -33
  14. package/android/src/main/java/com/reactnativestripesdk/utils/DefaultActivityLifecycleCallbacks.kt +41 -0
  15. package/android/src/oldarch/java/com/reactnativestripesdk/NativeOnrampSdkModuleSpec.java +17 -3
  16. package/android/src/oldarch/java/com/reactnativestripesdk/NativeStripeSdkModuleSpec.java +5 -1
  17. package/android/src/onramp/java/com/reactnativestripesdk/OnrampMappers.kt +116 -0
  18. package/android/src/onramp/java/com/reactnativestripesdk/OnrampSdkModule.kt +109 -1
  19. package/android/src/test/java/com/reactnativestripesdk/DrawableLoadingTest.kt +6 -2
  20. package/android/src/test/java/com/reactnativestripesdk/mappers/OnrampMappersTest.kt +107 -0
  21. package/android/src/test/java/com/reactnativestripesdk/utils/CheckoutMappersTest.kt +39 -30
  22. package/android/src/test/java/com/reactnativestripesdk/utils/CheckoutTestFixtures.java +8 -0
  23. package/ios/CurrencySelectorElementManager.m +7 -0
  24. package/ios/CurrencySelectorElementView.swift +142 -7
  25. package/ios/EmbeddedPaymentElementView.swift +1 -1
  26. package/ios/Mappers+Checkout.swift +251 -107
  27. package/ios/Mappers.swift +90 -2
  28. package/ios/NewArch/StripeCurrencySelectorElementComponentView.mm +14 -0
  29. package/ios/OldArch/StripeSdkEventEmitterCompat.h +1 -0
  30. package/ios/OldArch/StripeSdkEventEmitterCompat.m +6 -0
  31. package/ios/PaymentOptionDisplayData+ReactNative.swift +1 -1
  32. package/ios/PaymentSheetAppearance.swift +1 -1
  33. package/ios/StripeOnrampSdk.mm +19 -0
  34. package/ios/StripeSdk.mm +17 -4
  35. package/ios/StripeSdkEmitter.swift +1 -0
  36. package/ios/StripeSdkImpl+Checkout.swift +75 -23
  37. package/ios/StripeSdkImpl+CustomerSheet.swift +1 -1
  38. package/ios/StripeSdkImpl+Embedded.swift +1 -1
  39. package/ios/StripeSdkImpl+PaymentSheet.swift +1 -1
  40. package/ios/StripeSdkImpl.swift +88 -8
  41. package/lib/commonjs/components/PaymentMethodMessagingElement.js +1 -1
  42. package/lib/commonjs/components/PaymentMethodMessagingElement.js.map +1 -1
  43. package/lib/commonjs/hooks/useCheckout.js +1 -1
  44. package/lib/commonjs/hooks/useCheckout.js.map +1 -1
  45. package/lib/commonjs/hooks/useOnramp.js +1 -1
  46. package/lib/commonjs/hooks/useOnramp.js.map +1 -1
  47. package/lib/commonjs/specs/NativeOnrampSdkModule.js.map +1 -1
  48. package/lib/commonjs/specs/NativeStripeSdkModule.js.map +1 -1
  49. package/lib/module/components/PaymentMethodMessagingElement.js +1 -1
  50. package/lib/module/components/PaymentMethodMessagingElement.js.map +1 -1
  51. package/lib/module/hooks/useCheckout.js +1 -1
  52. package/lib/module/hooks/useCheckout.js.map +1 -1
  53. package/lib/module/hooks/useOnramp.js +1 -1
  54. package/lib/module/hooks/useOnramp.js.map +1 -1
  55. package/lib/module/specs/NativeOnrampSdkModule.js.map +1 -1
  56. package/lib/module/specs/NativeStripeSdkModule.js.map +1 -1
  57. package/lib/typescript/src/components/PaymentMethodMessagingElement.d.ts +1 -1
  58. package/lib/typescript/src/components/PaymentMethodMessagingElement.d.ts.map +1 -1
  59. package/lib/typescript/src/hooks/useCheckout.d.ts.map +1 -1
  60. package/lib/typescript/src/hooks/useOnramp.d.ts +22 -0
  61. package/lib/typescript/src/hooks/useOnramp.d.ts.map +1 -1
  62. package/lib/typescript/src/specs/NativeOnrampSdkModule.d.ts +3 -0
  63. package/lib/typescript/src/specs/NativeOnrampSdkModule.d.ts.map +1 -1
  64. package/lib/typescript/src/specs/NativeStripeSdkModule.d.ts +2 -1
  65. package/lib/typescript/src/specs/NativeStripeSdkModule.d.ts.map +1 -1
  66. package/lib/typescript/src/types/Checkout.d.ts +266 -84
  67. package/lib/typescript/src/types/Checkout.d.ts.map +1 -1
  68. package/lib/typescript/src/types/Onramp.d.ts +94 -0
  69. package/lib/typescript/src/types/Onramp.d.ts.map +1 -1
  70. package/package.json +3 -2
  71. package/src/components/PaymentMethodMessagingElement.tsx +54 -52
  72. package/src/hooks/useCheckout.tsx +20 -4
  73. package/src/hooks/useOnramp.tsx +44 -0
  74. package/src/specs/NativeOnrampSdkModule.ts +5 -0
  75. package/src/specs/NativeStripeSdkModule.ts +8 -1
  76. package/src/types/Checkout.ts +289 -84
  77. package/src/types/Onramp.ts +109 -0
  78. package/stripe-react-native.podspec +1 -1
  79. package/ios/CurrencySelectorElementManager.swift +0 -12
@@ -11,16 +11,6 @@
11
11
  <ID>CyclomaticComplexMethod:Mappers.kt:internal fun mapToPaymentMethodType: PaymentMethod.Type?</ID>
12
12
  <ID>CyclomaticComplexMethod:PaymentMethodCreateParamsFactory.kt:PaymentMethodCreateParamsFactory$@Throws(PaymentMethodCreateParamsException::class) fun createPaymentMethodParams: PaymentMethodCreateParams</ID>
13
13
  <ID>CyclomaticComplexMethod:PaymentSheetAppearance.kt:@SuppressLint("RestrictedApi") @Throws(PaymentSheetAppearanceException::class) private fun buildEmbeddedAppearance: PaymentSheet.Appearance.Embedded</ID>
14
- <ID>EmptyFunctionBlock:CardFieldView.kt:CardFieldView.&lt;no name provided>${ }</ID>
15
- <ID>EmptyFunctionBlock:CardFieldView.kt:CardFieldView.&lt;no name provided>${}</ID>
16
- <ID>EmptyFunctionBlock:CustomerSheetManager.kt:CustomerSheetManager.&lt;no name provided>${ }</ID>
17
- <ID>EmptyFunctionBlock:CustomerSheetManager.kt:CustomerSheetManager.&lt;no name provided>${}</ID>
18
- <ID>EmptyFunctionBlock:DrawableLoadingTest.kt:DrawableLoadingTest.MockAsyncDrawable${}</ID>
19
- <ID>EmptyFunctionBlock:PaymentSheetManager.kt:&lt;no name provided>${}</ID>
20
- <ID>EmptyFunctionBlock:PaymentSheetManager.kt:PaymentSheetManager.&lt;no name provided>${ }</ID>
21
- <ID>EmptyFunctionBlock:PaymentSheetManager.kt:PaymentSheetManager.&lt;no name provided>${}</ID>
22
- <ID>EmptyFunctionBlock:StripeAbstractComposeView.kt:StripeAbstractComposeView.CompatView${ }</ID>
23
- <ID>EmptyFunctionBlock:StripeSdkModule.kt:StripeSdkModule.&lt;no name provided>${}</ID>
24
14
  <ID>ForbiddenComment:Mappers.kt:// TODO: Can't access, private</ID>
25
15
  <ID>ForbiddenComment:PaymentSheetAppearance.kt:// TODO: The theme is so crazy long, why does each Color thing has the same redundant Theme...</ID>
26
16
  <ID>ForbiddenComment:PaymentSheetAppearance.kt:// TODO: Why is background a string but successBackgroundColor a "dynamic" color?</ID>
@@ -3,4 +3,4 @@ StripeSdk_compileSdkVersion=36
3
3
  StripeSdk_targetSdkVersion=36
4
4
  StripeSdk_minSdkVersion=23
5
5
  # Keep StripeSdk_stripeVersion in sync with https://github.com/stripe/stripe-identity-react-native/blob/main/android/gradle.properties
6
- StripeSdk_stripeVersion=23.6.+
6
+ StripeSdk_stripeVersion=23.9.+
@@ -367,32 +367,10 @@ class CardFieldView(
367
367
  }
368
368
  }
369
369
 
370
- mCardWidget.setCardInputListener(
371
- object : CardInputListener {
372
- override fun onCardComplete() {}
373
-
374
- override fun onExpirationComplete() {}
375
-
376
- override fun onCvcComplete() {}
377
-
378
- override fun onPostalCodeComplete() {}
379
-
380
- override fun onFocusChange(focusField: CardInputListener.FocusField) {}
381
- },
382
- )
370
+ mCardWidget.setCardInputListener(DefaultCardInputListener())
383
371
 
384
372
  mCardWidget.setExpiryDateTextWatcher(
385
- object : TextWatcher {
386
- override fun beforeTextChanged(
387
- p0: CharSequence?,
388
- p1: Int,
389
- p2: Int,
390
- p3: Int,
391
- ) {
392
- }
393
-
394
- override fun afterTextChanged(p0: Editable?) {}
395
-
373
+ object : DefaultTextWatcher() {
396
374
  override fun onTextChanged(
397
375
  var1: CharSequence?,
398
376
  var2: Int,
@@ -410,17 +388,7 @@ class CardFieldView(
410
388
  )
411
389
 
412
390
  mCardWidget.setPostalCodeTextWatcher(
413
- object : TextWatcher {
414
- override fun beforeTextChanged(
415
- p0: CharSequence?,
416
- p1: Int,
417
- p2: Int,
418
- p3: Int,
419
- ) {
420
- }
421
-
422
- override fun afterTextChanged(p0: Editable?) {}
423
-
391
+ object : DefaultTextWatcher() {
424
392
  override fun onTextChanged(
425
393
  var1: CharSequence?,
426
394
  var2: Int,
@@ -433,17 +401,7 @@ class CardFieldView(
433
401
  )
434
402
 
435
403
  mCardWidget.setCardNumberTextWatcher(
436
- object : TextWatcher {
437
- override fun beforeTextChanged(
438
- p0: CharSequence?,
439
- p1: Int,
440
- p2: Int,
441
- p3: Int,
442
- ) {
443
- }
444
-
445
- override fun afterTextChanged(p0: Editable?) {}
446
-
404
+ object : DefaultTextWatcher() {
447
405
  override fun onTextChanged(
448
406
  var1: CharSequence?,
449
407
  var2: Int,
@@ -458,17 +416,7 @@ class CardFieldView(
458
416
  )
459
417
 
460
418
  mCardWidget.setCvcNumberTextWatcher(
461
- object : TextWatcher {
462
- override fun beforeTextChanged(
463
- p0: CharSequence?,
464
- p1: Int,
465
- p2: Int,
466
- p3: Int,
467
- ) {
468
- }
469
-
470
- override fun afterTextChanged(p0: Editable?) {}
471
-
419
+ object : DefaultTextWatcher() {
472
420
  override fun onTextChanged(
473
421
  var1: CharSequence?,
474
422
  var2: Int,
@@ -534,4 +482,50 @@ class CardFieldView(
534
482
  private companion object {
535
483
  const val CARD_WIDGET_HORIZONTAL_PADDING = 20
536
484
  }
485
+
486
+ private open class DefaultTextWatcher : TextWatcher {
487
+ override fun beforeTextChanged(
488
+ p0: CharSequence?,
489
+ p1: Int,
490
+ p2: Int,
491
+ p3: Int,
492
+ ) {
493
+ // NO-OP
494
+ }
495
+
496
+ override fun afterTextChanged(p0: Editable?) {
497
+ // NO-OP
498
+ }
499
+
500
+ override fun onTextChanged(
501
+ var1: CharSequence?,
502
+ var2: Int,
503
+ var3: Int,
504
+ var4: Int,
505
+ ) {
506
+ // NO-OP
507
+ }
508
+ }
509
+
510
+ private open class DefaultCardInputListener : CardInputListener {
511
+ override fun onCardComplete() {
512
+ // NO-OP
513
+ }
514
+
515
+ override fun onExpirationComplete() {
516
+ // NO-OP
517
+ }
518
+
519
+ override fun onCvcComplete() {
520
+ // NO-OP
521
+ }
522
+
523
+ override fun onPostalCodeComplete() {
524
+ // NO-OP
525
+ }
526
+
527
+ override fun onFocusChange(focusField: CardInputListener.FocusField) {
528
+ // NO-OP
529
+ }
530
+ }
537
531
  }
@@ -0,0 +1,26 @@
1
+ package com.reactnativestripesdk
2
+
3
+ import com.facebook.react.bridge.WritableMap
4
+ import com.facebook.react.uimanager.events.Event
5
+
6
+ internal class CurrencySelectorElementEvent(
7
+ surfaceId: Int,
8
+ viewTag: Int,
9
+ private val eventType: EventType,
10
+ private val eventMap: WritableMap?,
11
+ ) : Event<CurrencySelectorElementEvent>(surfaceId, viewTag) {
12
+ enum class EventType {
13
+ OnHeightChange,
14
+ }
15
+
16
+ override fun getEventData() = eventMap
17
+
18
+ companion object {
19
+ const val ON_HEIGHT_CHANGE = "topHeightChange"
20
+ }
21
+
22
+ override fun getEventName(): String =
23
+ when (eventType) {
24
+ EventType.OnHeightChange -> ON_HEIGHT_CHANGE
25
+ }
26
+ }
@@ -1,26 +1,139 @@
1
1
  package com.reactnativestripesdk
2
2
 
3
3
  import android.annotation.SuppressLint
4
- import android.widget.FrameLayout
5
- import com.facebook.react.uimanager.ThemedReactContext
6
-
7
- /**
8
- * Minimal stub so the JS-side `CurrencySelectorElement` codegen mapping
9
- * resolves at app launch. The full Compose-based implementation that hosts
10
- * `Checkout.CurrencySelectorContent()` lives on the feature branch and will
11
- * replace this once that lands.
12
- */
13
- @SuppressLint("ViewConstructor")
4
+ import android.content.Context
5
+ import androidx.compose.foundation.layout.Box
6
+ import androidx.compose.runtime.Composable
7
+ import androidx.compose.runtime.LaunchedEffect
8
+ import androidx.compose.runtime.getValue
9
+ import androidx.compose.runtime.mutableStateOf
10
+ import androidx.compose.runtime.remember
11
+ import androidx.compose.runtime.setValue
12
+ import androidx.compose.ui.Modifier
13
+ import androidx.compose.ui.input.pointer.pointerInput
14
+ import androidx.compose.ui.layout.layout
15
+ import androidx.compose.ui.layout.onSizeChanged
16
+ import androidx.compose.ui.platform.LocalDensity
17
+ import androidx.compose.ui.unit.IntOffset
18
+ import androidx.compose.ui.unit.dp
19
+ import com.facebook.react.bridge.Arguments
20
+ import com.facebook.react.bridge.ReactContext
21
+ import com.facebook.react.bridge.WritableMap
22
+ import com.facebook.react.uimanager.UIManagerHelper
23
+ import com.stripe.android.paymentelement.CheckoutSessionPreview
24
+
25
+ @OptIn(CheckoutSessionPreview::class)
14
26
  class CurrencySelectorElementView(
15
- context: ThemedReactContext,
16
- ) : FrameLayout(context) {
17
- @Suppress("UnusedParameter")
27
+ context: Context,
28
+ ) : StripeAbstractComposeView(context) {
29
+ private val reactContext get() = context as ReactContext
30
+
31
+ // Backing state for props. mutableStateOf so Compose recomposes on changes.
32
+ private var sessionKeyState = mutableStateOf<String?>(null)
33
+ private var disabledState = mutableStateOf(false)
34
+
18
35
  fun setSessionKey(value: String?) {
19
- // no-op stub
36
+ sessionKeyState.value = value?.takeIf { it.isNotEmpty() }
20
37
  }
21
38
 
22
- @Suppress("UnusedParameter")
23
39
  fun setDisabled(value: Boolean) {
24
- // no-op stub
40
+ disabledState.value = value
41
+ }
42
+
43
+ @SuppressLint("RestrictedApi")
44
+ @Composable
45
+ override fun Content() {
46
+ val sessionKey by remember { sessionKeyState }
47
+ val disabled by remember { disabledState }
48
+
49
+ val checkout = remember(sessionKey) {
50
+ sessionKey?.let { key ->
51
+ val module = reactContext.getNativeModule(StripeSdkModule::class.java)
52
+ module?.checkoutInstances?.get(key)
53
+ }
54
+ }
55
+
56
+ if (checkout != null) {
57
+ // We don't push state to JS from this view — `StripeSdkModule` already
58
+ // observes the same `Checkout` instance and emits a unified
59
+ // `checkoutSessionDidChangeState` event that `useCheckout` mirrors into
60
+ // its `state`. Keeping the bridge in one place avoids redundant
61
+ // round-trips and inconsistencies.
62
+ MeasuredCurrencySelector(
63
+ reportHeightChange = { reportHeightChange(it) },
64
+ ) {
65
+ Box {
66
+ checkout.CurrencySelectorContent()
67
+ if (disabled) {
68
+ // The Stripe Android composable doesn't yet expose an `isEnabled`
69
+ // parameter, so swallow taps with a transparent pointer-input
70
+ // overlay while `disabled` is true. The toggle stays visible but
71
+ // can't be interacted with.
72
+ Box(
73
+ Modifier
74
+ .matchParentSize()
75
+ .pointerInput(Unit) { /* consume all gestures */ },
76
+ )
77
+ }
78
+ }
79
+ }
80
+ } else {
81
+ // No checkout yet: take up zero height so the element collapses.
82
+ LaunchedEffect(Unit) {
83
+ reportHeightChange(0f)
84
+ }
85
+ }
86
+ }
87
+
88
+ @Composable
89
+ private fun MeasuredCurrencySelector(
90
+ reportHeightChange: (Float) -> Unit,
91
+ content: @Composable () -> Unit,
92
+ ) {
93
+ val density = LocalDensity.current
94
+ var heightDp by remember { mutableStateOf(1.dp) } // non-zero sentinel
95
+
96
+ Box(
97
+ Modifier
98
+ .onSizeChanged { size ->
99
+ val h = with(density) { size.height.toDp() }
100
+ if (h != heightDp) {
101
+ heightDp = h
102
+ reportHeightChange(h.value)
103
+ }
104
+ }
105
+ .layout { measurable, constraints ->
106
+ val widthPx = constraints.maxWidth
107
+ val minHpx = measurable.minIntrinsicHeight(widthPx).coerceAtLeast(0)
108
+
109
+ val placeable = measurable.measure(
110
+ constraints.copy(minHeight = minHpx, maxHeight = minHpx),
111
+ )
112
+
113
+ layout(constraints.maxWidth, placeable.height) {
114
+ placeable.placeRelative(IntOffset.Zero)
115
+ }
116
+ },
117
+ ) {
118
+ content()
119
+ }
120
+ }
121
+
122
+ private fun reportHeightChange(height: Float) {
123
+ val params = Arguments.createMap().apply {
124
+ putDouble("height", height.toDouble())
125
+ }
126
+ dispatchEvent(CurrencySelectorElementEvent.EventType.OnHeightChange, params)
127
+ }
128
+
129
+ private fun dispatchEvent(
130
+ eventType: CurrencySelectorElementEvent.EventType,
131
+ params: WritableMap,
132
+ ) {
133
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
134
+ val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
135
+ dispatcher?.dispatchEvent(
136
+ CurrencySelectorElementEvent(surfaceId, id, eventType, params),
137
+ )
25
138
  }
26
139
  }
@@ -6,7 +6,9 @@ import com.facebook.react.uimanager.ViewGroupManager
6
6
  import com.facebook.react.uimanager.annotations.ReactProp
7
7
  import com.facebook.react.viewmanagers.StripeCurrencySelectorElementManagerDelegate
8
8
  import com.facebook.react.viewmanagers.StripeCurrencySelectorElementManagerInterface
9
+ import com.stripe.android.paymentelement.CheckoutSessionPreview
9
10
 
11
+ @OptIn(CheckoutSessionPreview::class)
10
12
  @ReactModule(name = CurrencySelectorElementViewManager.NAME)
11
13
  class CurrencySelectorElementViewManager :
12
14
  ViewGroupManager<CurrencySelectorElementView>(),
@@ -24,11 +26,26 @@ class CurrencySelectorElementViewManager :
24
26
  override fun createViewInstance(ctx: ThemedReactContext): CurrencySelectorElementView =
25
27
  CurrencySelectorElementView(ctx)
26
28
 
29
+ override fun onDropViewInstance(view: CurrencySelectorElementView) {
30
+ super.onDropViewInstance(view)
31
+ view.handleOnDropViewInstance()
32
+ }
33
+
34
+ override fun needsCustomLayoutForChildren(): Boolean = true
35
+
36
+ override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> =
37
+ mutableMapOf(
38
+ CurrencySelectorElementEvent.ON_HEIGHT_CHANGE to
39
+ mutableMapOf("registrationName" to "onHeightChange"),
40
+ )
41
+
27
42
  @ReactProp(name = "sessionKey")
28
43
  override fun setSessionKey(
29
44
  view: CurrencySelectorElementView,
30
45
  value: String?,
31
46
  ) {
47
+ // Codegen marks `sessionKey` as required, but it can briefly arrive as
48
+ // null when the view is recycled. Treat empty/null the same.
32
49
  view.setSessionKey(value)
33
50
  }
34
51
 
@@ -103,4 +103,8 @@ class EventEmitterCompat(
103
103
  fun emitOnCheckoutClientSecretRequested(value: ReadableMap?) {
104
104
  invoke("onCheckoutClientSecretRequested", value)
105
105
  }
106
+
107
+ fun emitCheckoutSessionDidChangeState(value: ReadableMap?) {
108
+ invoke("checkoutSessionDidChangeState", value)
109
+ }
106
110
  }
@@ -2,7 +2,6 @@ package com.reactnativestripesdk
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.app.Activity
5
- import android.app.Application
6
5
  import android.content.Intent
7
6
  import android.graphics.Bitmap
8
7
  import android.graphics.Canvas
@@ -21,6 +20,7 @@ import com.facebook.react.bridge.ReactApplicationContext
21
20
  import com.facebook.react.bridge.ReadableMap
22
21
  import com.facebook.react.bridge.WritableMap
23
22
  import com.reactnativestripesdk.addresssheet.AddressSheetView
23
+ import com.reactnativestripesdk.utils.DefaultActivityLifecycleCallbacks
24
24
  import com.reactnativestripesdk.utils.ErrorType
25
25
  import com.reactnativestripesdk.utils.KeepJsAwakeTask
26
26
  import com.reactnativestripesdk.utils.PaymentSheetAppearanceException
@@ -63,7 +63,6 @@ import kotlinx.coroutines.launch
63
63
  import kotlinx.coroutines.suspendCancellableCoroutine
64
64
  import kotlinx.coroutines.withTimeoutOrNull
65
65
  import java.io.ByteArrayOutputStream
66
- import kotlin.Exception
67
66
  import kotlin.coroutines.resume
68
67
 
69
68
  @OptIn(
@@ -468,7 +467,7 @@ class PaymentSheetManager(
468
467
  var paymentSheetActivity: Activity? = null
469
468
 
470
469
  val activityLifecycleCallbacks =
471
- object : Application.ActivityLifecycleCallbacks {
470
+ object : DefaultActivityLifecycleCallbacks() {
472
471
  override fun onActivityCreated(
473
472
  activity: Activity,
474
473
  savedInstanceState: Bundle?,
@@ -480,20 +479,6 @@ class PaymentSheetManager(
480
479
  }
481
480
  }
482
481
 
483
- override fun onActivityStarted(activity: Activity) {}
484
-
485
- override fun onActivityResumed(activity: Activity) {}
486
-
487
- override fun onActivityPaused(activity: Activity) {}
488
-
489
- override fun onActivityStopped(activity: Activity) {}
490
-
491
- override fun onActivitySaveInstanceState(
492
- activity: Activity,
493
- outState: Bundle,
494
- ) {
495
- }
496
-
497
482
  override fun onActivityDestroyed(activity: Activity) {
498
483
  if (activity.javaClass.name == PAYMENT_SHEET_ACTIVITY ||
499
484
  activity.javaClass.name == PAYMENT_OPTIONS_ACTIVITY
@@ -702,12 +687,16 @@ suspend fun waitForDrawableToLoad(
702
687
  who: Drawable,
703
688
  what: Runnable,
704
689
  `when`: Long,
705
- ) {}
690
+ ) {
691
+ // NO-OP
692
+ }
706
693
 
707
694
  override fun unscheduleDrawable(
708
695
  who: Drawable,
709
696
  what: Runnable,
710
- ) {}
697
+ ) {
698
+ // NO-OP
699
+ }
711
700
  }
712
701
 
713
702
  drawable.callback = callback
@@ -64,6 +64,7 @@ abstract class StripeAbstractComposeView(
64
64
 
65
65
  @Composable
66
66
  override fun Content() {
67
+ // NO-OP
67
68
  }
68
69
  }
69
70
 
@@ -2,7 +2,6 @@ package com.reactnativestripesdk
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.app.Activity
5
- import android.app.Application
6
5
  import android.content.Intent
7
6
  import android.os.Bundle
8
7
  import android.os.Handler
@@ -10,7 +9,6 @@ import android.os.Looper
10
9
  import android.util.Log
11
10
  import android.view.ViewGroup
12
11
  import androidx.appcompat.app.AppCompatActivity
13
- import androidx.browser.customtabs.CustomTabsIntent
14
12
  import androidx.core.net.toUri
15
13
  import androidx.fragment.app.FragmentActivity
16
14
  import com.facebook.react.ReactActivity
@@ -31,6 +29,7 @@ import com.reactnativestripesdk.customersheet.CustomerSheetManager
31
29
  import com.reactnativestripesdk.pushprovisioning.PushProvisioningProxy
32
30
  import com.reactnativestripesdk.utils.ConfirmPaymentErrorType
33
31
  import com.reactnativestripesdk.utils.CreateTokenErrorType
32
+ import com.reactnativestripesdk.utils.DefaultActivityLifecycleCallbacks
34
33
  import com.reactnativestripesdk.utils.ErrorType
35
34
  import com.reactnativestripesdk.utils.GooglePayErrorType
36
35
  import com.reactnativestripesdk.utils.RetrievePaymentIntentErrorType
@@ -82,10 +81,15 @@ import com.stripe.android.model.Token
82
81
  import com.stripe.android.paymentelement.CheckoutSessionPreview
83
82
  import com.stripe.android.payments.bankaccount.CollectBankAccountConfiguration
84
83
  import com.stripe.android.paymentsheet.PaymentSheet
84
+ import kotlinx.coroutines.CancellableContinuation
85
85
  import kotlinx.coroutines.CompletableDeferred
86
86
  import kotlinx.coroutines.CoroutineScope
87
87
  import kotlinx.coroutines.Dispatchers
88
+ import kotlinx.coroutines.Job
89
+ import kotlinx.coroutines.flow.combine
90
+ import kotlinx.coroutines.flow.distinctUntilChanged
88
91
  import kotlinx.coroutines.launch
92
+ import kotlinx.coroutines.suspendCancellableCoroutine
89
93
  import org.json.JSONObject
90
94
  import java.util.UUID
91
95
 
@@ -113,6 +117,13 @@ class StripeSdkModule(
113
117
  private var googlePayLauncherManager: GooglePayLauncherManager? = null
114
118
  private var googlePayPaymentMethodLauncherManager: GooglePayPaymentMethodLauncherManager? = null
115
119
  internal val checkoutInstances = mutableMapOf<String, Checkout>()
120
+ private val serverUpdateContinuations =
121
+ mutableMapOf<String, CancellableContinuation<Result<Unit>>>()
122
+
123
+ // / Tracks the long-running coroutine that observes each Checkout's
124
+ // / `checkoutSession` + `isLoading` flows and forwards transitions to JS as
125
+ // / `checkoutSessionDidChangeState` events.
126
+ private val checkoutStateObservers = mutableMapOf<String, Job>()
116
127
 
117
128
  private var customerSheetManager: CustomerSheetManager? = null
118
129
 
@@ -165,6 +176,8 @@ class StripeSdkModule(
165
176
 
166
177
  stripeUIManagers.forEach { it.destroy() }
167
178
  stripeUIManagers.clear()
179
+ checkoutStateObservers.values.forEach { it.cancel() }
180
+ checkoutStateObservers.clear()
168
181
  checkoutInstances.clear()
169
182
  }
170
183
 
@@ -1743,6 +1756,7 @@ class StripeSdkModule(
1743
1756
  onSuccess = { checkout ->
1744
1757
  val sessionKey = UUID.randomUUID().toString()
1745
1758
  checkoutInstances[sessionKey] = checkout
1759
+ observeCheckoutState(sessionKey, checkout)
1746
1760
 
1747
1761
  promise.resolve(
1748
1762
  Arguments.createMap().apply {
@@ -1866,13 +1880,48 @@ class StripeSdkModule(
1866
1880
  }
1867
1881
  }
1868
1882
 
1869
- override fun checkoutRefresh(
1883
+ override fun checkoutRunServerUpdateStart(
1870
1884
  sessionKey: String,
1871
1885
  promise: Promise,
1872
1886
  ) {
1873
- performCheckoutMutation(sessionKey, promise) { checkout ->
1874
- checkout.refresh()
1887
+ val checkout = checkoutInstances[sessionKey] ?: run {
1888
+ promise.reject(ErrorType.Failed.toString(), "Checkout session not found.")
1889
+ return
1890
+ }
1891
+
1892
+ if (serverUpdateContinuations.containsKey(sessionKey)) {
1893
+ promise.reject(ErrorType.Failed.toString(), "A server update is already in progress for this session.")
1894
+ return
1895
+ }
1896
+
1897
+ CoroutineScope(Dispatchers.Main).launch {
1898
+ checkout.runServerUpdate {
1899
+ suspendCancellableCoroutine { continuation ->
1900
+ serverUpdateContinuations[sessionKey] = continuation
1901
+ }
1902
+ }.fold(
1903
+ onSuccess = { promise.resolve(mapFromCheckoutState(checkout)) },
1904
+ onFailure = { promise.reject(ErrorType.Failed.toString(), it.message, it) },
1905
+ )
1906
+ }
1907
+ }
1908
+
1909
+ override fun checkoutRunServerUpdateComplete(
1910
+ sessionKey: String,
1911
+ error: String?,
1912
+ promise: Promise,
1913
+ ) {
1914
+ val continuation = serverUpdateContinuations.remove(sessionKey) ?: run {
1915
+ promise.reject(ErrorType.Failed.toString(), "No pending server update for this session.")
1916
+ return
1917
+ }
1918
+
1919
+ if (error != null) {
1920
+ continuation.resume(Result.failure(Exception(error))) {}
1921
+ } else {
1922
+ continuation.resume(Result.success(Unit)) {}
1875
1923
  }
1924
+ promise.resolve(null)
1876
1925
  }
1877
1926
 
1878
1927
  private fun buildCheckoutConfiguration(configuration: ReadableMap): Checkout.Configuration {
@@ -1884,6 +1933,41 @@ class StripeSdkModule(
1884
1933
  return checkoutConfiguration
1885
1934
  }
1886
1935
 
1936
+ /**
1937
+ * Forwards the checkout's `(checkoutSession, isLoading)` flow to JS as
1938
+ * `checkoutSessionDidChangeState` events. Equivalent to the iOS
1939
+ * `CheckoutDelegate` bridge — every native-side mutation (currency selection,
1940
+ * promo code, refresh, etc.) goes through one canonical channel that
1941
+ * `useCheckout` listens to.
1942
+ */
1943
+ private fun observeCheckoutState(
1944
+ sessionKey: String,
1945
+ checkout: Checkout,
1946
+ ) {
1947
+ checkoutStateObservers[sessionKey]?.cancel()
1948
+
1949
+ val job = CoroutineScope(Dispatchers.Main).launch {
1950
+ // `combine` re-emits on every upstream tick. `distinctUntilChanged`
1951
+ // collapses runs where neither field actually moved — both flows are
1952
+ // StateFlows so we'd otherwise see a redundant initial replay when
1953
+ // wiring up.
1954
+ combine(
1955
+ checkout.checkoutSession,
1956
+ checkout.isLoading,
1957
+ ) { session, isLoading -> session to isLoading }
1958
+ .distinctUntilChanged()
1959
+ .collect {
1960
+ eventEmitter.emitCheckoutSessionDidChangeState(
1961
+ Arguments.createMap().apply {
1962
+ putString("sessionKey", sessionKey)
1963
+ putMap("state", mapFromCheckoutState(checkout))
1964
+ },
1965
+ )
1966
+ }
1967
+ }
1968
+ checkoutStateObservers[sessionKey] = job
1969
+ }
1970
+
1887
1971
  private fun performCheckoutMutation(
1888
1972
  sessionKey: String,
1889
1973
  promise: Promise,
@@ -1925,14 +2009,14 @@ class StripeSdkModule(
1925
2009
  private var isRecreatingReactActivity = false
1926
2010
  private var isAuthWebViewActive = false
1927
2011
  private val activityLifecycleCallbacks =
1928
- object : Application.ActivityLifecycleCallbacks {
2012
+ object : DefaultActivityLifecycleCallbacks() {
1929
2013
  override fun onActivityCreated(
1930
2014
  activity: Activity,
1931
- bundle: Bundle?,
2015
+ savedInstanceState: Bundle?,
1932
2016
  ) {
1933
- // Only set flag when ReactActivity is actually being recreated (bundle != null)
1934
- // bundle != null means this is a recreation, not first creation
1935
- if (activity is ReactActivity && bundle != null) {
2017
+ // Only set flag when ReactActivity is actually being recreated (savedInstanceState != null)
2018
+ // savedInstanceState != null means this is a recreation, not first creation
2019
+ if (activity is ReactActivity && savedInstanceState != null) {
1936
2020
  isRecreatingReactActivity = true
1937
2021
  }
1938
2022
 
@@ -1951,21 +2035,6 @@ class StripeSdkModule(
1951
2035
  }
1952
2036
  }
1953
2037
  }
1954
-
1955
- override fun onActivityStarted(activity: Activity) {}
1956
-
1957
- override fun onActivityResumed(activity: Activity) {}
1958
-
1959
- override fun onActivityPaused(activity: Activity) {}
1960
-
1961
- override fun onActivityStopped(activity: Activity) {}
1962
-
1963
- override fun onActivitySaveInstanceState(
1964
- activity: Activity,
1965
- bundle: Bundle,
1966
- ) {}
1967
-
1968
- override fun onActivityDestroyed(activity: Activity) {}
1969
2038
  }
1970
2039
 
1971
2040
  /**
@@ -2006,7 +2075,7 @@ class StripeSdkModule(
2006
2075
  // Timeout for auth webview fallback (if JavaScript doesn't call authWebViewDeepLinkHandled)
2007
2076
  private const val AUTH_WEBVIEW_FALLBACK_TIMEOUT_MS = 60_000L
2008
2077
 
2009
- private const val FILE_CLEANUP_DELAY_MS = 3000L
2078
+ private const val FILE_CLEANUP_DELAY_MS = 3_000L
2010
2079
 
2011
2080
  // SDK-managed storage for pending stripe-connect:// URLs
2012
2081
  // This is static because deep links can arrive before ReactContext is available