@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.
- package/android/detekt/detekt-baseline.xml +0 -10
- package/android/gradle.properties +1 -1
- package/android/src/main/java/com/reactnativestripesdk/CardFieldView.kt +51 -57
- package/android/src/main/java/com/reactnativestripesdk/CurrencySelectorElementEvent.kt +26 -0
- package/android/src/main/java/com/reactnativestripesdk/CurrencySelectorElementView.kt +129 -16
- package/android/src/main/java/com/reactnativestripesdk/CurrencySelectorElementViewManager.kt +17 -0
- package/android/src/main/java/com/reactnativestripesdk/EventEmitterCompat.kt +4 -0
- package/android/src/main/java/com/reactnativestripesdk/PaymentSheetManager.kt +8 -19
- package/android/src/main/java/com/reactnativestripesdk/StripeAbstractComposeView.kt +1 -0
- package/android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt +95 -26
- package/android/src/main/java/com/reactnativestripesdk/StripeSdkPackage.kt +1 -0
- package/android/src/main/java/com/reactnativestripesdk/customersheet/CustomerSheetManager.kt +2 -16
- package/android/src/main/java/com/reactnativestripesdk/utils/CheckoutMappers.kt +59 -33
- package/android/src/main/java/com/reactnativestripesdk/utils/DefaultActivityLifecycleCallbacks.kt +41 -0
- package/android/src/oldarch/java/com/reactnativestripesdk/NativeOnrampSdkModuleSpec.java +17 -3
- package/android/src/oldarch/java/com/reactnativestripesdk/NativeStripeSdkModuleSpec.java +5 -1
- package/android/src/onramp/java/com/reactnativestripesdk/OnrampMappers.kt +116 -0
- package/android/src/onramp/java/com/reactnativestripesdk/OnrampSdkModule.kt +109 -1
- package/android/src/test/java/com/reactnativestripesdk/DrawableLoadingTest.kt +6 -2
- package/android/src/test/java/com/reactnativestripesdk/mappers/OnrampMappersTest.kt +107 -0
- package/android/src/test/java/com/reactnativestripesdk/utils/CheckoutMappersTest.kt +39 -30
- package/android/src/test/java/com/reactnativestripesdk/utils/CheckoutTestFixtures.java +8 -0
- package/ios/CurrencySelectorElementManager.m +7 -0
- package/ios/CurrencySelectorElementView.swift +142 -7
- package/ios/EmbeddedPaymentElementView.swift +1 -1
- package/ios/Mappers+Checkout.swift +251 -107
- package/ios/Mappers.swift +90 -2
- package/ios/NewArch/StripeCurrencySelectorElementComponentView.mm +14 -0
- package/ios/OldArch/StripeSdkEventEmitterCompat.h +1 -0
- package/ios/OldArch/StripeSdkEventEmitterCompat.m +6 -0
- package/ios/PaymentOptionDisplayData+ReactNative.swift +1 -1
- package/ios/PaymentSheetAppearance.swift +1 -1
- package/ios/StripeOnrampSdk.mm +19 -0
- package/ios/StripeSdk.mm +17 -4
- package/ios/StripeSdkEmitter.swift +1 -0
- package/ios/StripeSdkImpl+Checkout.swift +75 -23
- package/ios/StripeSdkImpl+CustomerSheet.swift +1 -1
- package/ios/StripeSdkImpl+Embedded.swift +1 -1
- package/ios/StripeSdkImpl+PaymentSheet.swift +1 -1
- package/ios/StripeSdkImpl.swift +88 -8
- package/lib/commonjs/components/PaymentMethodMessagingElement.js +1 -1
- package/lib/commonjs/components/PaymentMethodMessagingElement.js.map +1 -1
- package/lib/commonjs/hooks/useCheckout.js +1 -1
- package/lib/commonjs/hooks/useCheckout.js.map +1 -1
- package/lib/commonjs/hooks/useOnramp.js +1 -1
- package/lib/commonjs/hooks/useOnramp.js.map +1 -1
- package/lib/commonjs/specs/NativeOnrampSdkModule.js.map +1 -1
- package/lib/commonjs/specs/NativeStripeSdkModule.js.map +1 -1
- package/lib/module/components/PaymentMethodMessagingElement.js +1 -1
- package/lib/module/components/PaymentMethodMessagingElement.js.map +1 -1
- package/lib/module/hooks/useCheckout.js +1 -1
- package/lib/module/hooks/useCheckout.js.map +1 -1
- package/lib/module/hooks/useOnramp.js +1 -1
- package/lib/module/hooks/useOnramp.js.map +1 -1
- package/lib/module/specs/NativeOnrampSdkModule.js.map +1 -1
- package/lib/module/specs/NativeStripeSdkModule.js.map +1 -1
- package/lib/typescript/src/components/PaymentMethodMessagingElement.d.ts +1 -1
- package/lib/typescript/src/components/PaymentMethodMessagingElement.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useCheckout.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useOnramp.d.ts +22 -0
- package/lib/typescript/src/hooks/useOnramp.d.ts.map +1 -1
- package/lib/typescript/src/specs/NativeOnrampSdkModule.d.ts +3 -0
- package/lib/typescript/src/specs/NativeOnrampSdkModule.d.ts.map +1 -1
- package/lib/typescript/src/specs/NativeStripeSdkModule.d.ts +2 -1
- package/lib/typescript/src/specs/NativeStripeSdkModule.d.ts.map +1 -1
- package/lib/typescript/src/types/Checkout.d.ts +266 -84
- package/lib/typescript/src/types/Checkout.d.ts.map +1 -1
- package/lib/typescript/src/types/Onramp.d.ts +94 -0
- package/lib/typescript/src/types/Onramp.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/components/PaymentMethodMessagingElement.tsx +54 -52
- package/src/hooks/useCheckout.tsx +20 -4
- package/src/hooks/useOnramp.tsx +44 -0
- package/src/specs/NativeOnrampSdkModule.ts +5 -0
- package/src/specs/NativeStripeSdkModule.ts +8 -1
- package/src/types/Checkout.ts +289 -84
- package/src/types/Onramp.ts +109 -0
- package/stripe-react-native.podspec +1 -1
- package/ios/CurrencySelectorElementManager.swift +0 -12
|
@@ -4,12 +4,17 @@ package com.reactnativestripesdk.mappers
|
|
|
4
4
|
|
|
5
5
|
import android.annotation.SuppressLint
|
|
6
6
|
import androidx.compose.ui.graphics.toArgb
|
|
7
|
+
import com.reactnativestripesdk.ComplianceIdentifierFieldException
|
|
8
|
+
import com.reactnativestripesdk.InvalidIdentifiersArrayException
|
|
7
9
|
import com.reactnativestripesdk.mapAppearance
|
|
8
10
|
import com.reactnativestripesdk.mapConfig
|
|
9
11
|
import com.reactnativestripesdk.mapFromKycInfo
|
|
10
12
|
import com.reactnativestripesdk.mapGooglePayConfig
|
|
11
13
|
import com.reactnativestripesdk.mapPaymentDetailsType
|
|
14
|
+
import com.reactnativestripesdk.mapToComplianceIdentifiers
|
|
15
|
+
import com.reactnativestripesdk.utils.readableArrayOf
|
|
12
16
|
import com.reactnativestripesdk.utils.readableMapOf
|
|
17
|
+
import com.stripe.android.core.model.CountryCode
|
|
13
18
|
import com.stripe.android.crypto.onramp.ExperimentalCryptoOnramp
|
|
14
19
|
import com.stripe.android.crypto.onramp.model.KycInfo
|
|
15
20
|
import com.stripe.android.crypto.onramp.model.PaymentMethodDisplayData
|
|
@@ -22,12 +27,14 @@ import org.junit.Assert.assertEquals
|
|
|
22
27
|
import org.junit.Assert.assertFalse
|
|
23
28
|
import org.junit.Assert.assertNotNull
|
|
24
29
|
import org.junit.Assert.assertNull
|
|
30
|
+
import org.junit.Assert.assertThrows
|
|
25
31
|
import org.junit.Assert.assertTrue
|
|
26
32
|
import org.junit.Test
|
|
27
33
|
import org.junit.runner.RunWith
|
|
28
34
|
import org.robolectric.RobolectricTestRunner
|
|
29
35
|
|
|
30
36
|
@SuppressLint("RestrictedApi")
|
|
37
|
+
@Suppress("LargeClass")
|
|
31
38
|
@RunWith(RobolectricTestRunner::class)
|
|
32
39
|
class OnrampMappersTest {
|
|
33
40
|
@Test
|
|
@@ -460,6 +467,13 @@ class OnrampMappersTest {
|
|
|
460
467
|
state = "CA",
|
|
461
468
|
),
|
|
462
469
|
dateOfBirth = DateOfBirth(day = 15, month = 6, year = 1990),
|
|
470
|
+
birthCountry = CountryCode.create("FR"),
|
|
471
|
+
birthCity = "Paris",
|
|
472
|
+
nationalities =
|
|
473
|
+
listOf(
|
|
474
|
+
CountryCode.create("FR"),
|
|
475
|
+
CountryCode.create("DE"),
|
|
476
|
+
),
|
|
463
477
|
)
|
|
464
478
|
val result = mapFromKycInfo(kycInfo)
|
|
465
479
|
|
|
@@ -481,6 +495,14 @@ class OnrampMappersTest {
|
|
|
481
495
|
assertEquals(15, dob!!.getInt("day"))
|
|
482
496
|
assertEquals(6, dob.getInt("month"))
|
|
483
497
|
assertEquals(1990, dob.getInt("year"))
|
|
498
|
+
|
|
499
|
+
assertEquals("FR", result.getString("birthCountry"))
|
|
500
|
+
assertEquals("Paris", result.getString("birthCity"))
|
|
501
|
+
|
|
502
|
+
val nationalities = result.getArray("nationalities")
|
|
503
|
+
assertNotNull(nationalities)
|
|
504
|
+
assertEquals("FR", nationalities!!.getString(0))
|
|
505
|
+
assertEquals("DE", nationalities.getString(1))
|
|
484
506
|
}
|
|
485
507
|
|
|
486
508
|
@Test
|
|
@@ -492,6 +514,9 @@ class OnrampMappersTest {
|
|
|
492
514
|
idNumber = null,
|
|
493
515
|
address = null,
|
|
494
516
|
dateOfBirth = null,
|
|
517
|
+
birthCountry = null,
|
|
518
|
+
birthCity = null,
|
|
519
|
+
nationalities = null,
|
|
495
520
|
)
|
|
496
521
|
val result = mapFromKycInfo(kycInfo)
|
|
497
522
|
|
|
@@ -500,6 +525,9 @@ class OnrampMappersTest {
|
|
|
500
525
|
assertFalse(result.hasKey("idNumber"))
|
|
501
526
|
assertFalse(result.hasKey("address"))
|
|
502
527
|
assertFalse(result.hasKey("dateOfBirth"))
|
|
528
|
+
assertFalse(result.hasKey("birthCountry"))
|
|
529
|
+
assertFalse(result.hasKey("birthCity"))
|
|
530
|
+
assertFalse(result.hasKey("nationalities"))
|
|
503
531
|
}
|
|
504
532
|
|
|
505
533
|
@Test
|
|
@@ -511,6 +539,9 @@ class OnrampMappersTest {
|
|
|
511
539
|
idNumber = null,
|
|
512
540
|
address = null,
|
|
513
541
|
dateOfBirth = DateOfBirth(day = 1, month = 1, year = 2000),
|
|
542
|
+
birthCountry = CountryCode.create("IT"),
|
|
543
|
+
birthCity = "Rome",
|
|
544
|
+
nationalities = listOf(CountryCode.create("IT")),
|
|
514
545
|
)
|
|
515
546
|
val result = mapFromKycInfo(kycInfo)
|
|
516
547
|
|
|
@@ -524,6 +555,9 @@ class OnrampMappersTest {
|
|
|
524
555
|
assertEquals(1, dob!!.getInt("day"))
|
|
525
556
|
assertEquals(1, dob.getInt("month"))
|
|
526
557
|
assertEquals(2000, dob.getInt("year"))
|
|
558
|
+
assertEquals("IT", result.getString("birthCountry"))
|
|
559
|
+
assertEquals("Rome", result.getString("birthCity"))
|
|
560
|
+
assertEquals("IT", result.getArray("nationalities")!!.getString(0))
|
|
527
561
|
}
|
|
528
562
|
|
|
529
563
|
@Test
|
|
@@ -543,6 +577,9 @@ class OnrampMappersTest {
|
|
|
543
577
|
state = null,
|
|
544
578
|
),
|
|
545
579
|
dateOfBirth = null,
|
|
580
|
+
birthCountry = null,
|
|
581
|
+
birthCity = null,
|
|
582
|
+
nationalities = null,
|
|
546
583
|
)
|
|
547
584
|
val result = mapFromKycInfo(kycInfo)
|
|
548
585
|
|
|
@@ -555,4 +592,74 @@ class OnrampMappersTest {
|
|
|
555
592
|
assertFalse(address.hasKey("postalCode"))
|
|
556
593
|
assertFalse(address.hasKey("state"))
|
|
557
594
|
}
|
|
595
|
+
|
|
596
|
+
@Test
|
|
597
|
+
fun mapToComplianceIdentifiers_ValidIdentifiers_ReturnsIdentifiers() {
|
|
598
|
+
val identifiers =
|
|
599
|
+
readableArrayOf(
|
|
600
|
+
readableMapOf(
|
|
601
|
+
"type" to " AT_STN ",
|
|
602
|
+
"value" to " 123456789 ",
|
|
603
|
+
),
|
|
604
|
+
readableMapOf(
|
|
605
|
+
"type" to "DE_STN",
|
|
606
|
+
"value" to "987654321",
|
|
607
|
+
),
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
val result = mapToComplianceIdentifiers(identifiers)
|
|
611
|
+
|
|
612
|
+
assertEquals(2, result.size)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
@Test
|
|
616
|
+
fun mapToComplianceIdentifiers_BlankType_ThrowsFieldException() {
|
|
617
|
+
val identifiers =
|
|
618
|
+
readableArrayOf(
|
|
619
|
+
readableMapOf(
|
|
620
|
+
"type" to " ",
|
|
621
|
+
"value" to "123456789",
|
|
622
|
+
),
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
val error =
|
|
626
|
+
assertThrows(ComplianceIdentifierFieldException::class.java) {
|
|
627
|
+
mapToComplianceIdentifiers(identifiers)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
assertEquals("Invalid format for field: type", error.message)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
@Test
|
|
634
|
+
fun mapToComplianceIdentifiers_BlankValue_ThrowsFieldException() {
|
|
635
|
+
val identifiers =
|
|
636
|
+
readableArrayOf(
|
|
637
|
+
readableMapOf(
|
|
638
|
+
"type" to "AT_STN",
|
|
639
|
+
"value" to " ",
|
|
640
|
+
),
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
val error =
|
|
644
|
+
assertThrows(ComplianceIdentifierFieldException::class.java) {
|
|
645
|
+
mapToComplianceIdentifiers(identifiers)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
assertEquals("Invalid format for field: value", error.message)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
@Test
|
|
652
|
+
fun mapToComplianceIdentifiers_NonMapEntry_ThrowsInvalidArrayException() {
|
|
653
|
+
val identifiers = readableArrayOf("AT_STN")
|
|
654
|
+
|
|
655
|
+
val error =
|
|
656
|
+
assertThrows(InvalidIdentifiersArrayException::class.java) {
|
|
657
|
+
mapToComplianceIdentifiers(identifiers)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
assertEquals(
|
|
661
|
+
"Unexpected format of identifiers array. Expected dictionaries with String keys.",
|
|
662
|
+
error.message,
|
|
663
|
+
)
|
|
664
|
+
}
|
|
558
665
|
}
|
|
@@ -4,6 +4,7 @@ import com.stripe.android.paymentelement.CheckoutSessionPreview
|
|
|
4
4
|
import org.junit.Assert.assertEquals
|
|
5
5
|
import org.junit.Assert.assertFalse
|
|
6
6
|
import org.junit.Assert.assertNotNull
|
|
7
|
+
import org.junit.Assert.assertNull
|
|
7
8
|
import org.junit.Test
|
|
8
9
|
import org.junit.runner.RunWith
|
|
9
10
|
import org.robolectric.RobolectricTestRunner
|
|
@@ -12,7 +13,7 @@ import org.robolectric.RobolectricTestRunner
|
|
|
12
13
|
@OptIn(CheckoutSessionPreview::class)
|
|
13
14
|
class CheckoutMappersTest {
|
|
14
15
|
@Test
|
|
15
|
-
fun
|
|
16
|
+
fun mapFromCheckoutState_mapsLoadedSession() {
|
|
16
17
|
val mapped =
|
|
17
18
|
mapFromCheckoutState(
|
|
18
19
|
isLoading = false,
|
|
@@ -24,27 +25,27 @@ class CheckoutMappersTest {
|
|
|
24
25
|
val session = mapped.getMap("session")
|
|
25
26
|
assertNotNull(session)
|
|
26
27
|
assertEquals("cs_test_123", session!!.getString("id"))
|
|
27
|
-
assertEquals("unknown", session.getString("status"))
|
|
28
|
-
assertEquals("unknown", session.getString("paymentStatus"))
|
|
29
28
|
assertFalse(session.getBoolean("livemode"))
|
|
30
29
|
assertEquals("usd", session.getString("currency"))
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
val
|
|
37
|
-
assertNotNull(
|
|
38
|
-
assertEquals(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
30
|
+
|
|
31
|
+
val status = session.getMap("status")
|
|
32
|
+
assertNotNull(status)
|
|
33
|
+
assertEquals("open", status!!.getString("type"))
|
|
34
|
+
|
|
35
|
+
val tax = session.getMap("tax")
|
|
36
|
+
assertNotNull(tax)
|
|
37
|
+
assertEquals("ready", tax!!.getString("status"))
|
|
38
|
+
|
|
39
|
+
val total = session.getMap("total")
|
|
40
|
+
assertNotNull(total)
|
|
41
|
+
val subtotal = total!!.getMap("subtotal")
|
|
42
|
+
assertNotNull(subtotal)
|
|
43
|
+
assertEquals(5000.0, subtotal!!.getDouble("minorUnitsAmount"), 0.0)
|
|
44
|
+
assertEquals("", subtotal.getString("amount"))
|
|
45
|
+
|
|
46
|
+
val grandTotal = total.getMap("total")
|
|
47
|
+
assertNotNull(grandTotal)
|
|
48
|
+
assertEquals(4044.0, grandTotal!!.getDouble("minorUnitsAmount"), 0.0)
|
|
48
49
|
|
|
49
50
|
val lineItems = session.getArray("lineItems")
|
|
50
51
|
assertNotNull(lineItems)
|
|
@@ -52,19 +53,27 @@ class CheckoutMappersTest {
|
|
|
52
53
|
assertEquals("li_item1", lineItems.getMap(0)?.getString("id"))
|
|
53
54
|
assertEquals("Llama Figure", lineItems.getMap(0)?.getString("name"))
|
|
54
55
|
assertEquals(2, lineItems.getMap(0)?.getInt("quantity"))
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
assertFalse(lineItems.getMap(0)!!.hasKey("unitAmount"))
|
|
57
|
+
|
|
58
|
+
val item2UnitAmount = lineItems.getMap(1)?.getMap("unitAmount")
|
|
59
|
+
assertNotNull(item2UnitAmount)
|
|
60
|
+
assertEquals(2499.0, item2UnitAmount!!.getDouble("minorUnitsAmount"), 0.0)
|
|
58
61
|
|
|
59
62
|
val shippingOptions = session.getArray("shippingOptions")
|
|
60
63
|
assertNotNull(shippingOptions)
|
|
61
64
|
assertEquals(2, shippingOptions!!.size())
|
|
62
65
|
assertEquals("shr_standard", shippingOptions.getMap(0)?.getString("id"))
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
assertEquals("
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
val shippingAmount = shippingOptions.getMap(0)?.getMap("amount")
|
|
67
|
+
assertNotNull(shippingAmount)
|
|
68
|
+
assertEquals(500.0, shippingAmount!!.getDouble("minorUnitsAmount"), 0.0)
|
|
69
|
+
|
|
70
|
+
val discountAmounts = session.getArray("discountAmounts")
|
|
71
|
+
assertNotNull(discountAmounts)
|
|
72
|
+
assertEquals(0, discountAmounts!!.size())
|
|
73
|
+
|
|
74
|
+
val currencyOptions = session.getArray("currencyOptions")
|
|
75
|
+
assertNotNull(currencyOptions)
|
|
76
|
+
assertEquals(0, currencyOptions!!.size())
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
@Test
|
|
@@ -81,9 +90,9 @@ class CheckoutMappersTest {
|
|
|
81
90
|
assertNotNull(session)
|
|
82
91
|
assertEquals("cs_test_blank_currency", session!!.getString("id"))
|
|
83
92
|
assertFalse(session.hasKey("currency"))
|
|
84
|
-
|
|
93
|
+
assertNull(session.getMap("total"))
|
|
85
94
|
assertEquals(0, session.getArray("lineItems")!!.size())
|
|
86
95
|
assertEquals(0, session.getArray("shippingOptions")!!.size())
|
|
87
|
-
assertEquals(0, session.getArray("
|
|
96
|
+
assertEquals(0, session.getArray("discountAmounts")!!.size())
|
|
88
97
|
}
|
|
89
98
|
}
|
|
@@ -25,7 +25,11 @@ public final class CheckoutTestFixtures {
|
|
|
25
25
|
|
|
26
26
|
return new CheckoutSession(
|
|
27
27
|
"cs_test_123",
|
|
28
|
+
CheckoutSession.Status.Open,
|
|
29
|
+
false,
|
|
28
30
|
"usd",
|
|
31
|
+
null,
|
|
32
|
+
new CheckoutSession.Tax(CheckoutSession.Tax.Status.Ready),
|
|
29
33
|
totals,
|
|
30
34
|
Arrays.asList(
|
|
31
35
|
new CheckoutSession.LineItem("li_item1", "Llama Figure", 2, null, 1998L, 1998L),
|
|
@@ -40,8 +44,12 @@ public final class CheckoutTestFixtures {
|
|
|
40
44
|
public static CheckoutSession blankCurrencySession() {
|
|
41
45
|
return new CheckoutSession(
|
|
42
46
|
"cs_test_blank_currency",
|
|
47
|
+
CheckoutSession.Status.Open,
|
|
48
|
+
false,
|
|
43
49
|
"",
|
|
44
50
|
null,
|
|
51
|
+
new CheckoutSession.Tax(CheckoutSession.Tax.Status.Ready),
|
|
52
|
+
null,
|
|
45
53
|
Collections.<CheckoutSession.LineItem>emptyList(),
|
|
46
54
|
Collections.<CheckoutSession.ShippingRate>emptyList(),
|
|
47
55
|
(CurrencySelectorOptions) null);
|
|
@@ -1,24 +1,159 @@
|
|
|
1
|
+
//
|
|
2
|
+
// CurrencySelectorElementView.swift
|
|
3
|
+
// stripe-react-native
|
|
4
|
+
//
|
|
5
|
+
// Created by Nick Porter on 5/7/26.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Combine
|
|
1
9
|
import Foundation
|
|
10
|
+
@_spi(ReactNativeSDK) import StripePaymentSheet
|
|
2
11
|
import UIKit
|
|
3
12
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
13
|
+
@objc(StripeCurrencySelectorElementManager)
|
|
14
|
+
class StripeCurrencySelectorElementManager: RCTViewManager {
|
|
15
|
+
override static func requiresMainQueueSetup() -> Bool {
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override func view() -> UIView! {
|
|
20
|
+
return StripeCurrencySelectorElementContainerView(frame: .zero)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
@objc(StripeCurrencySelectorElementContainerView)
|
|
9
25
|
public class StripeCurrencySelectorElementContainerView: UIView {
|
|
10
|
-
@objc public var sessionKey: String?
|
|
11
26
|
|
|
12
|
-
|
|
27
|
+
// MARK: - Bridge-exposed Props
|
|
28
|
+
|
|
29
|
+
@objc public var sessionKey: String? {
|
|
30
|
+
didSet {
|
|
31
|
+
guard sessionKey != oldValue else { return }
|
|
32
|
+
attachToSessionIfPossible()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
13
35
|
|
|
36
|
+
@objc public var disabled: Bool = false {
|
|
37
|
+
didSet {
|
|
38
|
+
selectorView?.isEnabled = !disabled
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Fired whenever the inner selector's intrinsic height changes so RN
|
|
43
|
+
/// can match it. Session-state changes flow through the central
|
|
44
|
+
/// `checkoutSessionDidChangeState` event on `StripeSdk` (see
|
|
45
|
+
/// `CheckoutStateBridge`), so this view does not need to push state.
|
|
14
46
|
@objc public var onHeightChange: RCTDirectEventBlock?
|
|
15
47
|
|
|
16
|
-
|
|
48
|
+
// MARK: - Private Properties
|
|
49
|
+
|
|
50
|
+
private var selectorView: Checkout.CurrencySelectorView?
|
|
51
|
+
private var sessionCancellable: AnyCancellable?
|
|
52
|
+
private var lastReportedHeight: CGFloat = -1
|
|
53
|
+
|
|
54
|
+
// MARK: - Init
|
|
55
|
+
|
|
56
|
+
override public init(frame: CGRect) {
|
|
17
57
|
super.init(frame: frame)
|
|
58
|
+
backgroundColor = .clear
|
|
59
|
+
clipsToBounds = true
|
|
18
60
|
}
|
|
19
61
|
|
|
20
62
|
@available(*, unavailable)
|
|
21
63
|
required init?(coder _: NSCoder) {
|
|
22
64
|
fatalError("init(coder:) has not been implemented")
|
|
23
65
|
}
|
|
66
|
+
|
|
67
|
+
// MARK: - Layout
|
|
68
|
+
|
|
69
|
+
public override func layoutSubviews() {
|
|
70
|
+
super.layoutSubviews()
|
|
71
|
+
reportHeightIfNeeded()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// MARK: - Setup
|
|
75
|
+
|
|
76
|
+
private func attachToSessionIfPossible() {
|
|
77
|
+
Task { @MainActor in
|
|
78
|
+
guard let key = self.sessionKey,
|
|
79
|
+
let checkout = StripeSdkImpl.shared.checkoutInstances[key] else {
|
|
80
|
+
self.detach()
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Already showing a selector — leave it in place. Re-creating
|
|
85
|
+
// would interrupt the inner view's loading animation and reset
|
|
86
|
+
// its enabled state mid-API-call.
|
|
87
|
+
if self.selectorView != nil {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
self.installSelectorView(for: checkout)
|
|
92
|
+
self.subscribeForLayoutUpdates(on: checkout)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// We don't push state to JS here — `CheckoutStateBridge` already does
|
|
97
|
+
/// that for the whole session. This subscription only nudges Auto Layout
|
|
98
|
+
/// so the caption-text changes after a currency selection are reflected
|
|
99
|
+
/// in our reported height back to RN.
|
|
100
|
+
private func subscribeForLayoutUpdates(on checkout: Checkout) {
|
|
101
|
+
sessionCancellable = checkout.$state
|
|
102
|
+
.dropFirst()
|
|
103
|
+
.receive(on: DispatchQueue.main)
|
|
104
|
+
.sink { [weak self] _ in
|
|
105
|
+
guard let self else { return }
|
|
106
|
+
self.setNeedsLayout()
|
|
107
|
+
self.layoutIfNeeded()
|
|
108
|
+
self.reportHeightIfNeeded()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private func installSelectorView(for checkout: Checkout) {
|
|
113
|
+
let view = Checkout.CurrencySelectorView(checkout: checkout)
|
|
114
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
|
115
|
+
view.isEnabled = !disabled
|
|
116
|
+
addSubview(view)
|
|
117
|
+
|
|
118
|
+
NSLayoutConstraint.activate([
|
|
119
|
+
view.topAnchor.constraint(equalTo: topAnchor),
|
|
120
|
+
view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
121
|
+
view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
122
|
+
view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
123
|
+
])
|
|
124
|
+
|
|
125
|
+
selectorView = view
|
|
126
|
+
|
|
127
|
+
// Force an initial height report so RN can lay us out at zero
|
|
128
|
+
// when Adaptive Pricing is unavailable.
|
|
129
|
+
setNeedsLayout()
|
|
130
|
+
layoutIfNeeded()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func detach() {
|
|
134
|
+
sessionCancellable?.cancel()
|
|
135
|
+
sessionCancellable = nil
|
|
136
|
+
selectorView?.removeFromSuperview()
|
|
137
|
+
selectorView = nil
|
|
138
|
+
lastReportedHeight = -1
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// MARK: - Events
|
|
142
|
+
|
|
143
|
+
private func reportHeightIfNeeded() {
|
|
144
|
+
let desiredHeight: CGFloat
|
|
145
|
+
if let selectorView, !selectorView.isHidden {
|
|
146
|
+
desiredHeight = selectorView.systemLayoutSizeFitting(
|
|
147
|
+
CGSize(width: bounds.width, height: UIView.layoutFittingCompressedSize.height),
|
|
148
|
+
withHorizontalFittingPriority: .required,
|
|
149
|
+
verticalFittingPriority: .fittingSizeLevel
|
|
150
|
+
).height
|
|
151
|
+
} else {
|
|
152
|
+
desiredHeight = 0
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
guard desiredHeight != lastReportedHeight else { return }
|
|
156
|
+
lastReportedHeight = desiredHeight
|
|
157
|
+
onHeightChange?(["height": desiredHeight])
|
|
158
|
+
}
|
|
24
159
|
}
|