@gmisoftware/react-native-pay 0.0.12 → 0.0.14

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 (162) hide show
  1. package/README.md +223 -220
  2. package/android/src/main/java/com/margelo/nitro/pay/GooglePayRequestBuilder.kt +45 -12
  3. package/android/src/main/java/com/margelo/nitro/pay/HybridPaymentHandler.kt +27 -8
  4. package/ios/HybridPaymentHandler.swift +116 -9
  5. package/lib/hooks/__tests__/usePaymentCheckout.integration.test.d.ts +1 -0
  6. package/lib/hooks/__tests__/usePaymentCheckout.integration.test.js +191 -0
  7. package/lib/hooks/usePaymentCheckout.d.ts +47 -3
  8. package/lib/hooks/usePaymentCheckout.js +6 -4
  9. package/lib/plugin/__tests__/index.test.d.ts +1 -0
  10. package/lib/plugin/__tests__/index.test.js +33 -0
  11. package/lib/plugin/__tests__/withApplePay.test.d.ts +1 -0
  12. package/lib/plugin/__tests__/withApplePay.test.js +58 -0
  13. package/lib/plugin/__tests__/withGooglePay.test.d.ts +1 -0
  14. package/lib/plugin/__tests__/withGooglePay.test.js +45 -0
  15. package/lib/plugin/withApplePay.d.ts +1 -0
  16. package/lib/plugin/withApplePay.js +19 -4
  17. package/lib/types/Payment.d.ts +2 -1
  18. package/lib/utils/__tests__/paymentHelpers.test.d.ts +1 -0
  19. package/lib/utils/__tests__/paymentHelpers.test.js +75 -0
  20. package/lib/utils/paymentHelpers.d.ts +1 -4
  21. package/lib/utils/paymentHelpers.js +2 -5
  22. package/nitrogen/generated/android/NitroPay+autolinking.cmake +1 -1
  23. package/nitrogen/generated/android/NitroPay+autolinking.gradle +1 -1
  24. package/nitrogen/generated/android/NitroPayOnLoad.cpp +1 -1
  25. package/nitrogen/generated/android/NitroPayOnLoad.hpp +1 -1
  26. package/nitrogen/generated/android/c++/JCNContact.hpp +1 -1
  27. package/nitrogen/generated/android/c++/JCNContactType.hpp +1 -1
  28. package/nitrogen/generated/android/c++/JCNLabeledEmailAddress.hpp +1 -1
  29. package/nitrogen/generated/android/c++/JCNLabeledPhoneNumber.hpp +1 -1
  30. package/nitrogen/generated/android/c++/JCNLabeledPostalAddress.hpp +1 -1
  31. package/nitrogen/generated/android/c++/JCNPhoneNumber.hpp +1 -1
  32. package/nitrogen/generated/android/c++/JCNPostalAddress.hpp +1 -1
  33. package/nitrogen/generated/android/c++/JFunc_void.hpp +1 -1
  34. package/nitrogen/generated/android/c++/JGooglePayButtonTheme.hpp +1 -1
  35. package/nitrogen/generated/android/c++/JGooglePayButtonType.hpp +1 -1
  36. package/nitrogen/generated/android/c++/JGooglePayEnvironment.hpp +1 -1
  37. package/nitrogen/generated/android/c++/JHybridGooglePayButtonSpec.cpp +1 -1
  38. package/nitrogen/generated/android/c++/JHybridGooglePayButtonSpec.hpp +1 -1
  39. package/nitrogen/generated/android/c++/JHybridPaymentHandlerSpec.cpp +1 -1
  40. package/nitrogen/generated/android/c++/JHybridPaymentHandlerSpec.hpp +1 -1
  41. package/nitrogen/generated/android/c++/JPKSecureElementPass.hpp +1 -1
  42. package/nitrogen/generated/android/c++/JPassActivationState.hpp +1 -1
  43. package/nitrogen/generated/android/c++/JPayServiceStatus.hpp +1 -1
  44. package/nitrogen/generated/android/c++/JPaymentItem.hpp +1 -1
  45. package/nitrogen/generated/android/c++/JPaymentItemType.hpp +1 -1
  46. package/nitrogen/generated/android/c++/JPaymentMethod.hpp +1 -1
  47. package/nitrogen/generated/android/c++/JPaymentMethodType.hpp +1 -1
  48. package/nitrogen/generated/android/c++/JPaymentNetwork.hpp +1 -1
  49. package/nitrogen/generated/android/c++/JPaymentRequest.hpp +10 -6
  50. package/nitrogen/generated/android/c++/JPaymentResult.hpp +1 -1
  51. package/nitrogen/generated/android/c++/JPaymentToken.hpp +1 -1
  52. package/nitrogen/generated/android/c++/views/JHybridGooglePayButtonStateUpdater.cpp +1 -1
  53. package/nitrogen/generated/android/c++/views/JHybridGooglePayButtonStateUpdater.hpp +1 -1
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/CNContact.kt +1 -1
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/CNContactType.kt +1 -1
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/CNLabeledEmailAddress.kt +1 -1
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/CNLabeledPhoneNumber.kt +1 -1
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/CNLabeledPostalAddress.kt +1 -1
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/CNPhoneNumber.kt +1 -1
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/CNPostalAddress.kt +1 -1
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/Func_void.kt +1 -1
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/GooglePayButtonTheme.kt +1 -1
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/GooglePayButtonType.kt +1 -1
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/GooglePayEnvironment.kt +1 -1
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/HybridGooglePayButtonSpec.kt +1 -1
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/HybridPaymentHandlerSpec.kt +1 -1
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/NitroPayOnLoad.kt +1 -1
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/PKSecureElementPass.kt +1 -1
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/PassActivationState.kt +1 -1
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/PayServiceStatus.kt +1 -1
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/PaymentItem.kt +1 -1
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/PaymentItemType.kt +1 -1
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/PaymentMethod.kt +1 -1
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/PaymentMethodType.kt +1 -1
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/PaymentNetwork.kt +1 -1
  76. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/PaymentRequest.kt +7 -4
  77. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/PaymentResult.kt +1 -1
  78. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/PaymentToken.kt +1 -1
  79. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/views/HybridGooglePayButtonManager.kt +1 -1
  80. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pay/views/HybridGooglePayButtonStateUpdater.kt +1 -1
  81. package/nitrogen/generated/ios/NitroPay+autolinking.rb +1 -1
  82. package/nitrogen/generated/ios/NitroPay-Swift-Cxx-Bridge.cpp +1 -1
  83. package/nitrogen/generated/ios/NitroPay-Swift-Cxx-Bridge.hpp +1 -1
  84. package/nitrogen/generated/ios/NitroPay-Swift-Cxx-Umbrella.hpp +1 -1
  85. package/nitrogen/generated/ios/NitroPayAutolinking.mm +1 -1
  86. package/nitrogen/generated/ios/NitroPayAutolinking.swift +1 -1
  87. package/nitrogen/generated/ios/c++/HybridApplePayButtonSpecSwift.cpp +1 -1
  88. package/nitrogen/generated/ios/c++/HybridApplePayButtonSpecSwift.hpp +1 -1
  89. package/nitrogen/generated/ios/c++/HybridPaymentHandlerSpecSwift.cpp +1 -1
  90. package/nitrogen/generated/ios/c++/HybridPaymentHandlerSpecSwift.hpp +1 -1
  91. package/nitrogen/generated/ios/c++/views/HybridApplePayButtonComponent.mm +1 -1
  92. package/nitrogen/generated/ios/swift/ApplePayButtonStyle.swift +1 -1
  93. package/nitrogen/generated/ios/swift/ApplePayButtonType.swift +1 -1
  94. package/nitrogen/generated/ios/swift/CNContact.swift +1 -1
  95. package/nitrogen/generated/ios/swift/CNContactType.swift +1 -1
  96. package/nitrogen/generated/ios/swift/CNLabeledEmailAddress.swift +1 -1
  97. package/nitrogen/generated/ios/swift/CNLabeledPhoneNumber.swift +1 -1
  98. package/nitrogen/generated/ios/swift/CNLabeledPostalAddress.swift +1 -1
  99. package/nitrogen/generated/ios/swift/CNPhoneNumber.swift +1 -1
  100. package/nitrogen/generated/ios/swift/CNPostalAddress.swift +1 -1
  101. package/nitrogen/generated/ios/swift/Func_void.swift +1 -1
  102. package/nitrogen/generated/ios/swift/Func_void_PaymentResult.swift +1 -1
  103. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +1 -1
  104. package/nitrogen/generated/ios/swift/GooglePayEnvironment.swift +1 -1
  105. package/nitrogen/generated/ios/swift/HybridApplePayButtonSpec.swift +1 -1
  106. package/nitrogen/generated/ios/swift/HybridApplePayButtonSpec_cxx.swift +1 -1
  107. package/nitrogen/generated/ios/swift/HybridPaymentHandlerSpec.swift +1 -1
  108. package/nitrogen/generated/ios/swift/HybridPaymentHandlerSpec_cxx.swift +1 -1
  109. package/nitrogen/generated/ios/swift/PKSecureElementPass.swift +1 -1
  110. package/nitrogen/generated/ios/swift/PassActivationState.swift +1 -1
  111. package/nitrogen/generated/ios/swift/PayServiceStatus.swift +1 -1
  112. package/nitrogen/generated/ios/swift/PaymentItem.swift +1 -1
  113. package/nitrogen/generated/ios/swift/PaymentItemType.swift +1 -1
  114. package/nitrogen/generated/ios/swift/PaymentMethod.swift +1 -1
  115. package/nitrogen/generated/ios/swift/PaymentMethodType.swift +1 -1
  116. package/nitrogen/generated/ios/swift/PaymentNetwork.swift +1 -1
  117. package/nitrogen/generated/ios/swift/PaymentRequest.swift +55 -6
  118. package/nitrogen/generated/ios/swift/PaymentResult.swift +1 -1
  119. package/nitrogen/generated/ios/swift/PaymentToken.swift +1 -1
  120. package/nitrogen/generated/shared/c++/ApplePayButtonStyle.hpp +1 -1
  121. package/nitrogen/generated/shared/c++/ApplePayButtonType.hpp +1 -1
  122. package/nitrogen/generated/shared/c++/CNContact.hpp +1 -1
  123. package/nitrogen/generated/shared/c++/CNContactType.hpp +1 -1
  124. package/nitrogen/generated/shared/c++/CNLabeledEmailAddress.hpp +1 -1
  125. package/nitrogen/generated/shared/c++/CNLabeledPhoneNumber.hpp +1 -1
  126. package/nitrogen/generated/shared/c++/CNLabeledPostalAddress.hpp +1 -1
  127. package/nitrogen/generated/shared/c++/CNPhoneNumber.hpp +1 -1
  128. package/nitrogen/generated/shared/c++/CNPostalAddress.hpp +1 -1
  129. package/nitrogen/generated/shared/c++/GooglePayButtonTheme.hpp +1 -1
  130. package/nitrogen/generated/shared/c++/GooglePayButtonType.hpp +1 -1
  131. package/nitrogen/generated/shared/c++/GooglePayEnvironment.hpp +1 -1
  132. package/nitrogen/generated/shared/c++/HybridApplePayButtonSpec.cpp +1 -1
  133. package/nitrogen/generated/shared/c++/HybridApplePayButtonSpec.hpp +1 -1
  134. package/nitrogen/generated/shared/c++/HybridGooglePayButtonSpec.cpp +1 -1
  135. package/nitrogen/generated/shared/c++/HybridGooglePayButtonSpec.hpp +1 -1
  136. package/nitrogen/generated/shared/c++/HybridPaymentHandlerSpec.cpp +1 -1
  137. package/nitrogen/generated/shared/c++/HybridPaymentHandlerSpec.hpp +1 -1
  138. package/nitrogen/generated/shared/c++/PKSecureElementPass.hpp +1 -1
  139. package/nitrogen/generated/shared/c++/PassActivationState.hpp +1 -1
  140. package/nitrogen/generated/shared/c++/PayServiceStatus.hpp +1 -1
  141. package/nitrogen/generated/shared/c++/PaymentItem.hpp +1 -1
  142. package/nitrogen/generated/shared/c++/PaymentItemType.hpp +1 -1
  143. package/nitrogen/generated/shared/c++/PaymentMethod.hpp +1 -1
  144. package/nitrogen/generated/shared/c++/PaymentMethodType.hpp +1 -1
  145. package/nitrogen/generated/shared/c++/PaymentNetwork.hpp +1 -1
  146. package/nitrogen/generated/shared/c++/PaymentRequest.hpp +10 -6
  147. package/nitrogen/generated/shared/c++/PaymentResult.hpp +1 -1
  148. package/nitrogen/generated/shared/c++/PaymentToken.hpp +1 -1
  149. package/nitrogen/generated/shared/c++/views/HybridApplePayButtonComponent.cpp +1 -1
  150. package/nitrogen/generated/shared/c++/views/HybridApplePayButtonComponent.hpp +1 -1
  151. package/nitrogen/generated/shared/c++/views/HybridGooglePayButtonComponent.cpp +1 -1
  152. package/nitrogen/generated/shared/c++/views/HybridGooglePayButtonComponent.hpp +1 -1
  153. package/package.json +21 -4
  154. package/src/hooks/__tests__/usePaymentCheckout.integration.test.ts +248 -0
  155. package/src/hooks/usePaymentCheckout.ts +68 -9
  156. package/src/plugin/__tests__/index.test.ts +37 -0
  157. package/src/plugin/__tests__/withApplePay.test.ts +83 -0
  158. package/src/plugin/__tests__/withGooglePay.test.ts +66 -0
  159. package/src/plugin/withApplePay.ts +34 -6
  160. package/src/types/Payment.ts +4 -1
  161. package/src/utils/__tests__/paymentHelpers.test.ts +127 -0
  162. package/src/utils/paymentHelpers.ts +30 -15
@@ -3,22 +3,27 @@ package com.margelo.nitro.pay
3
3
  import com.google.android.gms.wallet.WalletConstants
4
4
  import org.json.JSONArray
5
5
  import org.json.JSONObject
6
+ import java.util.Locale
6
7
 
7
8
  /**
8
9
  * Builder for Google Pay API request objects
9
10
  */
10
11
  object GooglePayRequestBuilder {
12
+ private val googlePayPriceLocale = Locale.US
11
13
 
12
14
  /**
13
15
  * Creates an IsReadyToPay request
14
16
  */
15
- fun createIsReadyToPayRequest(): JSONObject {
17
+ fun createIsReadyToPayRequest(existingPaymentMethodRequired: Boolean? = null): JSONObject {
16
18
  return JSONObject().apply {
17
19
  put("apiVersion", PaymentConstants.API_VERSION)
18
20
  put("apiVersionMinor", PaymentConstants.API_VERSION_MINOR)
19
21
  put("allowedPaymentMethods", JSONArray().apply {
20
22
  put(createBaseCardPaymentMethod())
21
23
  })
24
+ existingPaymentMethodRequired?.let {
25
+ put("existingPaymentMethodRequired", it)
26
+ }
22
27
  }
23
28
  }
24
29
 
@@ -29,6 +34,8 @@ object GooglePayRequestBuilder {
29
34
  request: PaymentRequest,
30
35
  environment: Int
31
36
  ): JSONObject {
37
+ validatePaymentRequest(request, environment)
38
+
32
39
  return JSONObject().apply {
33
40
  put("apiVersion", PaymentConstants.API_VERSION)
34
41
  put("apiVersionMinor", PaymentConstants.API_VERSION_MINOR)
@@ -51,7 +58,7 @@ object GooglePayRequestBuilder {
51
58
  put("merchantName", request.merchantName ?: PaymentConstants.DEFAULT_MERCHANT_NAME)
52
59
  // Add merchant ID only for PRODUCTION environment
53
60
  if (environment == WalletConstants.ENVIRONMENT_PRODUCTION) {
54
- put("merchantId", request.merchantIdentifier)
61
+ put("merchantId", request.googlePayMerchantId)
55
62
  }
56
63
  }
57
64
  }
@@ -88,20 +95,42 @@ object GooglePayRequestBuilder {
88
95
  request: PaymentRequest,
89
96
  environment: Int
90
97
  ): JSONObject {
98
+ val isProduction = environment == WalletConstants.ENVIRONMENT_PRODUCTION
99
+ val gateway = if (isProduction) {
100
+ request.googlePayGateway!!.trim()
101
+ } else {
102
+ request.googlePayGateway?.trim().takeUnless { it.isNullOrEmpty() }
103
+ ?: PaymentConstants.DEFAULT_GATEWAY
104
+ }
105
+ val gatewayMerchantId = if (isProduction) {
106
+ request.googlePayGatewayMerchantId!!.trim()
107
+ } else {
108
+ request.googlePayGatewayMerchantId?.trim().takeUnless { it.isNullOrEmpty() }
109
+ ?: PaymentConstants.DEFAULT_GATEWAY_MERCHANT_ID
110
+ }
111
+
91
112
  return JSONObject().apply {
92
113
  put("type", PaymentConstants.TOKENIZATION_PAYMENT_GATEWAY)
93
114
  put("parameters", JSONObject().apply {
94
- val isProduction = environment == WalletConstants.ENVIRONMENT_PRODUCTION
95
- put("gateway", request.googlePayGateway ?: PaymentConstants.DEFAULT_GATEWAY)
96
- put(
97
- "gatewayMerchantId",
98
- request.googlePayGatewayMerchantId
99
- ?: if (isProduction) request.merchantIdentifier
100
- else PaymentConstants.DEFAULT_GATEWAY_MERCHANT_ID
101
- )
115
+ put("gateway", gateway)
116
+ put("gatewayMerchantId", gatewayMerchantId)
102
117
  })
103
118
  }
104
119
  }
120
+
121
+ private fun validatePaymentRequest(request: PaymentRequest, environment: Int) {
122
+ if (environment != WalletConstants.ENVIRONMENT_PRODUCTION) return
123
+
124
+ require(!request.googlePayMerchantId.isNullOrBlank()) {
125
+ "googlePayMerchantId is required in PRODUCTION"
126
+ }
127
+ require(!request.googlePayGateway.isNullOrBlank()) {
128
+ "googlePayGateway is required in PRODUCTION"
129
+ }
130
+ require(!request.googlePayGatewayMerchantId.isNullOrBlank()) {
131
+ "googlePayGatewayMerchantId is required in PRODUCTION"
132
+ }
133
+ }
105
134
 
106
135
  /**
107
136
  * Creates transaction info
@@ -111,7 +140,7 @@ object GooglePayRequestBuilder {
111
140
 
112
141
  return JSONObject().apply {
113
142
  put("totalPriceStatus", PaymentConstants.TOTAL_PRICE_STATUS_FINAL)
114
- put("totalPrice", String.format("%.2f", totalAmount))
143
+ put("totalPrice", formatPrice(totalAmount))
115
144
  put("totalPriceLabel", PaymentConstants.TOTAL_PRICE_LABEL_DEFAULT)
116
145
  put("currencyCode", request.currencyCode)
117
146
  put("countryCode", request.countryCode)
@@ -138,11 +167,15 @@ object GooglePayRequestBuilder {
138
167
  else
139
168
  PaymentConstants.PENDING_TYPE
140
169
  )
141
- put("price", String.format("%.2f", item.amount))
170
+ put("price", formatPrice(item.amount))
142
171
  })
143
172
  }
144
173
  }
145
174
  }
175
+
176
+ private fun formatPrice(amount: Double): String {
177
+ return String.format(googlePayPriceLocale, "%.2f", amount)
178
+ }
146
179
 
147
180
  /**
148
181
  * Creates allowed auth methods
@@ -5,6 +5,7 @@ import android.content.Intent
5
5
  import android.util.Log
6
6
  import com.facebook.react.bridge.ActivityEventListener
7
7
  import com.facebook.react.bridge.ReactApplicationContext
8
+ import com.google.android.gms.tasks.Tasks
8
9
  import com.google.android.gms.wallet.AutoResolveHelper
9
10
  import com.google.android.gms.wallet.IsReadyToPayRequest
10
11
  import com.google.android.gms.wallet.PaymentData
@@ -14,6 +15,7 @@ import com.google.android.gms.wallet.Wallet
14
15
  import com.google.android.gms.wallet.WalletConstants
15
16
  import com.margelo.nitro.core.Promise
16
17
  import com.margelo.nitro.NitroModules
18
+ import java.util.concurrent.TimeUnit
17
19
 
18
20
  /**
19
21
  * Hybrid implementation of PaymentHandler for Google Pay on Android
@@ -38,15 +40,23 @@ class HybridPaymentHandler : HybridPaymentHandlerSpec(), ActivityEventListener {
38
40
 
39
41
  override fun payServiceStatus(): PayServiceStatus {
40
42
  return try {
41
- val request = IsReadyToPayRequest.fromJson(
42
- GooglePayRequestBuilder.createIsReadyToPayRequest().toString()
43
- )
44
-
45
43
  val client = createPaymentsClient(currentEnvironment)
46
- client.isReadyToPay(request) // Trigger async check
47
-
48
- // Return optimistic status (actual check is async)
49
- PayServiceStatus(canMakePayments = true, canSetupCards = true)
44
+ val canSetupCards = isReadyToPay(
45
+ client = client,
46
+ requestJson = GooglePayRequestBuilder.createIsReadyToPayRequest(
47
+ existingPaymentMethodRequired = false
48
+ )
49
+ )
50
+ val canMakePayments = isReadyToPay(
51
+ client = client,
52
+ requestJson = GooglePayRequestBuilder.createIsReadyToPayRequest(
53
+ existingPaymentMethodRequired = true
54
+ )
55
+ )
56
+ PayServiceStatus(
57
+ canMakePayments = canMakePayments,
58
+ canSetupCards = canSetupCards
59
+ )
50
60
  } catch (e: Exception) {
51
61
  Log.e(PaymentConstants.TAG_PAYMENT_HANDLER, "Error checking status", e)
52
62
  PayServiceStatus(canMakePayments = false, canSetupCards = false)
@@ -143,6 +153,15 @@ class HybridPaymentHandler : HybridPaymentHandlerSpec(), ActivityEventListener {
143
153
  GooglePayEnvironment.TEST, null -> WalletConstants.ENVIRONMENT_TEST
144
154
  }
145
155
  }
156
+
157
+ private fun isReadyToPay(
158
+ client: PaymentsClient,
159
+ requestJson: org.json.JSONObject
160
+ ): Boolean {
161
+ val request = IsReadyToPayRequest.fromJson(requestJson.toString())
162
+ val task = client.isReadyToPay(request)
163
+ return Tasks.await(task, 5, TimeUnit.SECONDS) ?: false
164
+ }
146
165
 
147
166
  private fun launchPaymentUI(
148
167
  paymentDataRequest: org.json.JSONObject,
@@ -6,21 +6,31 @@ import NitroModules
6
6
 
7
7
  private enum ErrorMessage {
8
8
  static let paymentCancelled = "Payment cancelled by user"
9
+ static let paymentDismissed = "Payment sheet was dismissed before authorization"
10
+ static let missingMerchantIdentifier = "No Apple Pay merchant identifier configured"
9
11
  static let unableToPresent = "Unable to present payment authorization"
10
12
  static let unableToCreate = "Unable to create payment authorization"
11
13
  }
12
14
 
15
+ private enum BundleKey {
16
+ static let applePayMerchantIdentifiers = "ReactNativePayApplePayMerchantIdentifiers"
17
+ }
18
+
13
19
  // MARK: - Payment Request Builder
14
20
 
15
21
  private struct PaymentRequestBuilder {
16
22
 
17
- static func build(from request: PaymentRequest) -> PKPaymentRequest {
23
+ static func build(from request: PaymentRequest) -> PKPaymentRequest? {
24
+ guard let merchantIdentifier = resolveMerchantIdentifier(from: request) else {
25
+ return nil
26
+ }
27
+
18
28
  let paymentRequest = PKPaymentRequest()
19
29
 
20
- paymentRequest.merchantIdentifier = request.merchantIdentifier
30
+ paymentRequest.merchantIdentifier = merchantIdentifier
21
31
  paymentRequest.countryCode = request.countryCode
22
32
  paymentRequest.currencyCode = request.currencyCode
23
- paymentRequest.paymentSummaryItems = buildPaymentItems(request.paymentItems)
33
+ paymentRequest.paymentSummaryItems = buildPaymentItems(for: request)
24
34
  paymentRequest.merchantCapabilities = buildMerchantCapabilities(request.merchantCapabilities)
25
35
  paymentRequest.supportedNetworks = buildSupportedNetworks(request.supportedNetworks)
26
36
 
@@ -32,9 +42,33 @@ private struct PaymentRequestBuilder {
32
42
 
33
43
  return paymentRequest
34
44
  }
45
+
46
+ private static func resolveMerchantIdentifier(from request: PaymentRequest) -> String? {
47
+ if let overrideIdentifier = request.applePayMerchantIdentifier?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines),
48
+ !overrideIdentifier.isEmpty {
49
+ return overrideIdentifier
50
+ }
51
+
52
+ if let merchantIdentifiers = Bundle.main.object(
53
+ forInfoDictionaryKey: BundleKey.applePayMerchantIdentifiers
54
+ ) as? [String] {
55
+ return merchantIdentifiers.first {
56
+ !$0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty
57
+ }
58
+ }
59
+
60
+ if let merchantIdentifier = Bundle.main.object(
61
+ forInfoDictionaryKey: BundleKey.applePayMerchantIdentifiers
62
+ ) as? String {
63
+ let trimmedIdentifier = merchantIdentifier.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
64
+ return trimmedIdentifier.isEmpty ? nil : trimmedIdentifier
65
+ }
66
+
67
+ return nil
68
+ }
35
69
 
36
- private static func buildPaymentItems(_ items: [PaymentItem]) -> [PKPaymentSummaryItem] {
37
- return items.map { item in
70
+ private static func buildPaymentItems(for request: PaymentRequest) -> [PKPaymentSummaryItem] {
71
+ let lineItems = request.paymentItems.map { item in
38
72
  let pkItem = PKPaymentSummaryItem(
39
73
  label: item.label,
40
74
  amount: NSDecimalNumber(decimal: Decimal(item.amount))
@@ -42,6 +76,29 @@ private struct PaymentRequestBuilder {
42
76
  pkItem.type = item.type == .final ? .final : .pending
43
77
  return pkItem
44
78
  }
79
+
80
+ guard request.paymentItems.count > 1 else {
81
+ return lineItems
82
+ }
83
+
84
+ let totalAmount = request.paymentItems.reduce(Decimal.zero) { partialResult, item in
85
+ partialResult + Decimal(item.amount)
86
+ }
87
+ let totalLabel: String
88
+ if let merchantName = request.merchantName?
89
+ .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines),
90
+ !merchantName.isEmpty {
91
+ totalLabel = merchantName
92
+ } else {
93
+ totalLabel = "Total"
94
+ }
95
+ let totalItem = PKPaymentSummaryItem(
96
+ label: totalLabel,
97
+ amount: NSDecimalNumber(decimal: totalAmount)
98
+ )
99
+ totalItem.type = .final
100
+
101
+ return lineItems + [totalItem]
45
102
  }
46
103
 
47
104
  private static func buildMerchantCapabilities(_ capabilities: [String]) -> PKMerchantCapability {
@@ -129,10 +186,16 @@ private struct PaymentTokenConverter {
129
186
  private class PaymentDelegate: NSObject, PKPaymentAuthorizationViewControllerDelegate {
130
187
  private weak var paymentHandler: HybridPaymentHandler?
131
188
  private var paymentAuthorized: Bool = false
189
+ private var presentationDate: Date?
190
+ private let userDismissThreshold: TimeInterval = 0.75
132
191
 
133
192
  init(paymentHandler: HybridPaymentHandler) {
134
193
  self.paymentHandler = paymentHandler
135
194
  }
195
+
196
+ func markPresented() {
197
+ presentationDate = Date()
198
+ }
136
199
 
137
200
  func paymentAuthorizationViewController(
138
201
  _ controller: PKPaymentAuthorizationViewController,
@@ -163,11 +226,19 @@ private class PaymentDelegate: NSObject, PKPaymentAuthorizationViewControllerDel
163
226
  func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
164
227
  controller.dismiss(animated: true) {
165
228
  if !self.paymentAuthorized {
229
+ let errorMessage: String
230
+ if let presentationDate = self.presentationDate,
231
+ Date().timeIntervalSince(presentationDate) >= self.userDismissThreshold {
232
+ errorMessage = ErrorMessage.paymentCancelled
233
+ } else {
234
+ errorMessage = ErrorMessage.paymentDismissed
235
+ }
236
+
166
237
  let result = PaymentResult.init(
167
238
  success: false,
168
239
  transactionId: nil,
169
240
  token: nil,
170
- error: ErrorMessage.paymentCancelled
241
+ error: errorMessage
171
242
  )
172
243
  self.paymentHandler?.handlePaymentResult(result)
173
244
  }
@@ -216,7 +287,10 @@ class HybridPaymentHandler: HybridPaymentHandlerSpec {
216
287
  // MARK: - Private Methods
217
288
 
218
289
  private func performPayment(request: PaymentRequest, completion: @escaping (PaymentResult) -> Void) {
219
- let paymentRequest = PaymentRequestBuilder.build(from: request)
290
+ guard let paymentRequest = PaymentRequestBuilder.build(from: request) else {
291
+ completion(createErrorResult(ErrorMessage.missingMerchantIdentifier))
292
+ return
293
+ }
220
294
 
221
295
  paymentCompletion = completion
222
296
  currentPaymentRequest = paymentRequest
@@ -229,12 +303,14 @@ class HybridPaymentHandler: HybridPaymentHandlerSpec {
229
303
 
230
304
  paymentAuthVC.delegate = delegate
231
305
 
232
- guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
306
+ guard let rootViewController = getRootViewController() else {
233
307
  completion(createErrorResult(ErrorMessage.unableToPresent))
234
308
  return
235
309
  }
236
310
 
237
- rootViewController.present(paymentAuthVC, animated: true)
311
+ rootViewController.present(paymentAuthVC, animated: true) {
312
+ self.delegate?.markPresented()
313
+ }
238
314
  }
239
315
 
240
316
  private func createErrorResult(_ error: String) -> PaymentResult {
@@ -245,4 +321,35 @@ class HybridPaymentHandler: HybridPaymentHandlerSpec {
245
321
  error: error
246
322
  )
247
323
  }
324
+
325
+ private func getRootViewController() -> UIViewController? {
326
+ let scenes = UIApplication.shared.connectedScenes
327
+ .compactMap { $0 as? UIWindowScene }
328
+ .filter { $0.activationState == .foregroundActive }
329
+
330
+ let rootViewController = scenes
331
+ .flatMap(\.windows)
332
+ .first(where: \.isKeyWindow)?
333
+ .rootViewController
334
+
335
+ return topViewController(from: rootViewController)
336
+ }
337
+
338
+ private func topViewController(from viewController: UIViewController?) -> UIViewController? {
339
+ guard let viewController else { return nil }
340
+
341
+ if let navigationController = viewController as? UINavigationController {
342
+ return topViewController(from: navigationController.visibleViewController)
343
+ }
344
+
345
+ if let tabBarController = viewController as? UITabBarController {
346
+ return topViewController(from: tabBarController.selectedViewController)
347
+ }
348
+
349
+ if let presentedViewController = viewController.presentedViewController {
350
+ return topViewController(from: presentedViewController)
351
+ }
352
+
353
+ return viewController
354
+ }
248
355
  }
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const react_native_1 = require("@testing-library/react-native");
4
+ const mockPayServiceStatus = jest.fn();
5
+ const mockStartPayment = jest.fn();
6
+ jest.mock('react-native-nitro-modules', () => ({
7
+ NitroModules: {
8
+ createHybridObject: jest.fn(() => ({
9
+ payServiceStatus: (...args) => mockPayServiceStatus(...args),
10
+ startPayment: (...args) => mockStartPayment(...args),
11
+ })),
12
+ },
13
+ }));
14
+ const usePaymentCheckout_1 = require("../usePaymentCheckout");
15
+ describe('usePaymentCheckout integration', () => {
16
+ beforeEach(() => {
17
+ mockPayServiceStatus.mockReturnValue({
18
+ canMakePayments: true,
19
+ canSetupCards: true,
20
+ });
21
+ mockStartPayment.mockReset();
22
+ });
23
+ const renderCheckout = () => (0, react_native_1.renderHook)(() => (0, usePaymentCheckout_1.usePaymentCheckout)({}));
24
+ it('loads payment service status on mount', async () => {
25
+ const { result } = renderCheckout();
26
+ await (0, react_native_1.waitFor)(() => {
27
+ expect(result.current.isCheckingStatus).toBe(false);
28
+ });
29
+ expect(result.current.canMakePayments).toBe(true);
30
+ expect(result.current.canSetupCards).toBe(true);
31
+ });
32
+ it('falls back to unavailable status when status check fails', async () => {
33
+ mockPayServiceStatus.mockImplementationOnce(() => {
34
+ throw new Error('Native status failure');
35
+ });
36
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
37
+ const { result } = renderCheckout();
38
+ await (0, react_native_1.waitFor)(() => {
39
+ expect(result.current.isCheckingStatus).toBe(false);
40
+ });
41
+ expect(result.current.canMakePayments).toBe(false);
42
+ expect(result.current.canSetupCards).toBe(false);
43
+ expect(consoleSpy).toHaveBeenCalled();
44
+ consoleSpy.mockRestore();
45
+ });
46
+ it('manages cart operations and total correctly', async () => {
47
+ const { result } = renderCheckout();
48
+ await (0, react_native_1.waitFor)(() => {
49
+ expect(result.current.isCheckingStatus).toBe(false);
50
+ });
51
+ (0, react_native_1.act)(() => {
52
+ result.current.addItem('Coffee', 4.5);
53
+ result.current.addItems([
54
+ { label: 'Sandwich', amount: 8.25 },
55
+ { label: 'Tip', amount: 1.25, type: 'pending' },
56
+ ]);
57
+ });
58
+ expect(result.current.items).toEqual([
59
+ { label: 'Coffee', amount: 4.5, type: 'final' },
60
+ { label: 'Sandwich', amount: 8.25, type: 'final' },
61
+ { label: 'Tip', amount: 1.25, type: 'pending' },
62
+ ]);
63
+ expect(result.current.total).toBeCloseTo(14);
64
+ (0, react_native_1.act)(() => {
65
+ result.current.updateItem(0, { amount: 5 });
66
+ });
67
+ expect(result.current.items[0]?.amount).toBe(5);
68
+ expect(result.current.total).toBeCloseTo(14.5);
69
+ (0, react_native_1.act)(() => {
70
+ result.current.removeItem(1);
71
+ });
72
+ expect(result.current.items.map((item) => item.label)).toEqual([
73
+ 'Coffee',
74
+ 'Tip',
75
+ ]);
76
+ (0, react_native_1.act)(() => {
77
+ result.current.clearItems();
78
+ });
79
+ expect(result.current.items).toEqual([]);
80
+ expect(result.current.total).toBe(0);
81
+ });
82
+ it('provides default fallback payment item when cart is empty', () => {
83
+ const { result } = renderCheckout();
84
+ expect(result.current.paymentRequest.paymentItems).toEqual([
85
+ { label: 'Total', amount: 0, type: 'final' },
86
+ ]);
87
+ expect(result.current.paymentRequest.countryCode).toBe('US');
88
+ expect(result.current.paymentRequest.currencyCode).toBe('USD');
89
+ });
90
+ it('rejects startPayment when cart is empty', async () => {
91
+ const { result } = renderCheckout();
92
+ let paymentResult = 'not-called';
93
+ await (0, react_native_1.act)(async () => {
94
+ paymentResult = await result.current.startPayment();
95
+ });
96
+ expect(paymentResult).toBeNull();
97
+ expect(result.current.error?.message).toBe('Cart is empty');
98
+ expect(mockStartPayment).not.toHaveBeenCalled();
99
+ });
100
+ it('handles successful payments', async () => {
101
+ const nativeSuccess = { success: true, transactionId: 'tx_123' };
102
+ mockStartPayment.mockResolvedValueOnce(nativeSuccess);
103
+ const { result } = renderCheckout();
104
+ (0, react_native_1.act)(() => {
105
+ result.current.addItem('Coffee', 4.99);
106
+ });
107
+ let paymentResult = null;
108
+ await (0, react_native_1.act)(async () => {
109
+ paymentResult = await result.current.startPayment();
110
+ });
111
+ expect(mockStartPayment).toHaveBeenCalledTimes(1);
112
+ expect(mockStartPayment).toHaveBeenCalledWith(expect.objectContaining({
113
+ paymentItems: [{ label: 'Coffee', amount: 4.99, type: 'final' }],
114
+ }));
115
+ expect(paymentResult).toEqual(nativeSuccess);
116
+ expect(result.current.result).toEqual(nativeSuccess);
117
+ expect(result.current.error).toBeNull();
118
+ expect(result.current.isProcessing).toBe(false);
119
+ });
120
+ it('sets an error when native payment response has success=false', async () => {
121
+ mockStartPayment.mockResolvedValueOnce({
122
+ success: false,
123
+ error: 'Declined by issuer',
124
+ });
125
+ const { result } = renderCheckout();
126
+ (0, react_native_1.act)(() => {
127
+ result.current.addItem('Order', 9.99);
128
+ });
129
+ await (0, react_native_1.act)(async () => {
130
+ await result.current.startPayment();
131
+ });
132
+ expect(result.current.error?.message).toBe('Declined by issuer');
133
+ expect(result.current.result).toEqual({
134
+ success: false,
135
+ error: 'Declined by issuer',
136
+ });
137
+ });
138
+ it('forwards Apple Pay override and Google Pay config when provided', async () => {
139
+ mockStartPayment.mockResolvedValueOnce({ success: true });
140
+ const { result } = (0, react_native_1.renderHook)(() => (0, usePaymentCheckout_1.usePaymentCheckout)({
141
+ applePayMerchantIdentifier: 'merchant.com.apple.override',
142
+ googlePayMerchantId: 'google-pay-merchant-id',
143
+ googlePayEnvironment: 'PRODUCTION',
144
+ googlePayGateway: 'stripe',
145
+ googlePayGatewayMerchantId: 'gateway-merchant-id',
146
+ }));
147
+ (0, react_native_1.act)(() => {
148
+ result.current.addItem('Order', 12);
149
+ });
150
+ await (0, react_native_1.act)(async () => {
151
+ await result.current.startPayment();
152
+ });
153
+ expect(mockStartPayment).toHaveBeenCalledWith(expect.objectContaining({
154
+ applePayMerchantIdentifier: 'merchant.com.apple.override',
155
+ googlePayMerchantId: 'google-pay-merchant-id',
156
+ googlePayEnvironment: 'PRODUCTION',
157
+ googlePayGateway: 'stripe',
158
+ googlePayGatewayMerchantId: 'gateway-merchant-id',
159
+ }));
160
+ });
161
+ it('uses generic error for non-Error thrown values', async () => {
162
+ mockStartPayment.mockRejectedValueOnce('native crash');
163
+ const { result } = renderCheckout();
164
+ (0, react_native_1.act)(() => {
165
+ result.current.addItem('Order', 12);
166
+ });
167
+ let paymentResult = 'not-null';
168
+ await (0, react_native_1.act)(async () => {
169
+ paymentResult = await result.current.startPayment();
170
+ });
171
+ expect(paymentResult).toBeNull();
172
+ expect(result.current.error?.message).toBe('Payment processing failed');
173
+ });
174
+ it('resets transient checkout state', async () => {
175
+ mockStartPayment.mockResolvedValueOnce({ success: false, error: 'Failed' });
176
+ const { result } = renderCheckout();
177
+ (0, react_native_1.act)(() => {
178
+ result.current.addItem('Order', 3);
179
+ });
180
+ await (0, react_native_1.act)(async () => {
181
+ await result.current.startPayment();
182
+ });
183
+ expect(result.current.error).not.toBeNull();
184
+ (0, react_native_1.act)(() => {
185
+ result.current.reset();
186
+ });
187
+ expect(result.current.error).toBeNull();
188
+ expect(result.current.result).toBeNull();
189
+ expect(result.current.isProcessing).toBe(false);
190
+ });
191
+ });
@@ -1,13 +1,56 @@
1
1
  import type { PaymentRequest, PaymentResult, PaymentItem, GooglePayEnvironment } from '../types';
2
+ /**
3
+ * Configuration for `usePaymentCheckout`.
4
+ */
2
5
  export interface UsePaymentCheckoutConfig {
3
- merchantIdentifier: string;
6
+ /**
7
+ * Merchant name shown by payment providers that support displaying it.
8
+ * Used primarily by Google Pay.
9
+ */
4
10
  merchantName?: string;
11
+ /**
12
+ * ISO 3166-1 alpha-2 country code for the transaction.
13
+ * Defaults to `'US'`.
14
+ */
5
15
  countryCode?: string;
16
+ /**
17
+ * ISO 4217 currency code for all payment items.
18
+ * Defaults to `'USD'`.
19
+ */
6
20
  currencyCode?: string;
21
+ /**
22
+ * Supported card networks for the payment sheet.
23
+ * Defaults to `['visa', 'mastercard', 'amex', 'discover']`.
24
+ */
7
25
  supportedNetworks?: string[];
26
+ /**
27
+ * Merchant capabilities passed to Apple Pay.
28
+ * Defaults to `['3DS']`.
29
+ */
8
30
  merchantCapabilities?: string[];
31
+ /**
32
+ * Optional Apple Pay Merchant ID override.
33
+ * When omitted, iOS reads the Merchant ID from the app entitlements.
34
+ */
35
+ applePayMerchantIdentifier?: string;
36
+ /**
37
+ * Google Pay merchant ID used in Android production requests.
38
+ * Not needed for iOS.
39
+ */
40
+ googlePayMerchantId?: string;
41
+ /**
42
+ * Google Pay environment for Android requests.
43
+ * Use `'TEST'` for sandbox flows and `'PRODUCTION'` for live payments.
44
+ */
9
45
  googlePayEnvironment?: GooglePayEnvironment;
46
+ /**
47
+ * Payment gateway identifier for Google Pay tokenization.
48
+ * Examples include `'stripe'`, `'braintree'`, and `'adyen'`.
49
+ */
10
50
  googlePayGateway?: string;
51
+ /**
52
+ * Merchant ID provided by your payment gateway for Google Pay tokenization.
53
+ */
11
54
  googlePayGatewayMerchantId?: string;
12
55
  }
13
56
  export interface UsePaymentCheckoutReturn {
@@ -41,7 +84,8 @@ export interface UsePaymentCheckoutReturn {
41
84
  * - Payment processing
42
85
  * - State management
43
86
  *
44
- * @param config - Payment configuration
87
+ * @param config - Hook configuration including locale, supported networks,
88
+ * Apple Pay override, and Google Pay gateway settings.
45
89
  * @returns Complete payment checkout interface
46
90
  *
47
91
  * @example
@@ -58,9 +102,9 @@ export interface UsePaymentCheckoutReturn {
58
102
  * isProcessing,
59
103
  * error,
60
104
  * } = usePaymentCheckout({
61
- * merchantIdentifier: 'merchant.com.example',
62
105
  * currencyCode: 'USD',
63
106
  * countryCode: 'US',
107
+ * googlePayMerchantId: 'your_google_pay_merchant_id',
64
108
  * })
65
109
  *
66
110
  * // Add single item