@gmisoftware/react-native-pay 0.0.11 → 0.0.13

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 +37 -10
  3. package/android/src/main/java/com/margelo/nitro/pay/HybridPaymentHandler.kt +27 -8
  4. package/ios/HybridPaymentHandler.swift +90 -6
  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 +13 -7
  154. package/src/hooks/__tests__/usePaymentCheckout.integration.test.ts +244 -0
  155. package/src/hooks/usePaymentCheckout.ts +60 -5
  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 +93 -0
  162. package/src/utils/paymentHelpers.ts +1 -6
@@ -12,13 +12,16 @@ object GooglePayRequestBuilder {
12
12
  /**
13
13
  * Creates an IsReadyToPay request
14
14
  */
15
- fun createIsReadyToPayRequest(): JSONObject {
15
+ fun createIsReadyToPayRequest(existingPaymentMethodRequired: Boolean? = null): JSONObject {
16
16
  return JSONObject().apply {
17
17
  put("apiVersion", PaymentConstants.API_VERSION)
18
18
  put("apiVersionMinor", PaymentConstants.API_VERSION_MINOR)
19
19
  put("allowedPaymentMethods", JSONArray().apply {
20
20
  put(createBaseCardPaymentMethod())
21
21
  })
22
+ existingPaymentMethodRequired?.let {
23
+ put("existingPaymentMethodRequired", it)
24
+ }
22
25
  }
23
26
  }
24
27
 
@@ -29,6 +32,8 @@ object GooglePayRequestBuilder {
29
32
  request: PaymentRequest,
30
33
  environment: Int
31
34
  ): JSONObject {
35
+ validatePaymentRequest(request, environment)
36
+
32
37
  return JSONObject().apply {
33
38
  put("apiVersion", PaymentConstants.API_VERSION)
34
39
  put("apiVersionMinor", PaymentConstants.API_VERSION_MINOR)
@@ -51,7 +56,7 @@ object GooglePayRequestBuilder {
51
56
  put("merchantName", request.merchantName ?: PaymentConstants.DEFAULT_MERCHANT_NAME)
52
57
  // Add merchant ID only for PRODUCTION environment
53
58
  if (environment == WalletConstants.ENVIRONMENT_PRODUCTION) {
54
- put("merchantId", request.merchantIdentifier)
59
+ put("merchantId", request.googlePayMerchantId)
55
60
  }
56
61
  }
57
62
  }
@@ -88,20 +93,42 @@ object GooglePayRequestBuilder {
88
93
  request: PaymentRequest,
89
94
  environment: Int
90
95
  ): JSONObject {
96
+ val isProduction = environment == WalletConstants.ENVIRONMENT_PRODUCTION
97
+ val gateway = if (isProduction) {
98
+ request.googlePayGateway!!.trim()
99
+ } else {
100
+ request.googlePayGateway?.trim().takeUnless { it.isNullOrEmpty() }
101
+ ?: PaymentConstants.DEFAULT_GATEWAY
102
+ }
103
+ val gatewayMerchantId = if (isProduction) {
104
+ request.googlePayGatewayMerchantId!!.trim()
105
+ } else {
106
+ request.googlePayGatewayMerchantId?.trim().takeUnless { it.isNullOrEmpty() }
107
+ ?: PaymentConstants.DEFAULT_GATEWAY_MERCHANT_ID
108
+ }
109
+
91
110
  return JSONObject().apply {
92
111
  put("type", PaymentConstants.TOKENIZATION_PAYMENT_GATEWAY)
93
112
  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
- )
113
+ put("gateway", gateway)
114
+ put("gatewayMerchantId", gatewayMerchantId)
102
115
  })
103
116
  }
104
117
  }
118
+
119
+ private fun validatePaymentRequest(request: PaymentRequest, environment: Int) {
120
+ if (environment != WalletConstants.ENVIRONMENT_PRODUCTION) return
121
+
122
+ require(!request.googlePayMerchantId.isNullOrBlank()) {
123
+ "googlePayMerchantId is required in PRODUCTION"
124
+ }
125
+ require(!request.googlePayGateway.isNullOrBlank()) {
126
+ "googlePayGateway is required in PRODUCTION"
127
+ }
128
+ require(!request.googlePayGatewayMerchantId.isNullOrBlank()) {
129
+ "googlePayGatewayMerchantId is required in PRODUCTION"
130
+ }
131
+ }
105
132
 
106
133
  /**
107
134
  * Creates transaction info
@@ -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,18 +6,28 @@ 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
33
  paymentRequest.paymentSummaryItems = buildPaymentItems(request.paymentItems)
@@ -32,6 +42,30 @@ 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
70
  private static func buildPaymentItems(_ items: [PaymentItem]) -> [PKPaymentSummaryItem] {
37
71
  return items.map { item in
@@ -129,10 +163,16 @@ private struct PaymentTokenConverter {
129
163
  private class PaymentDelegate: NSObject, PKPaymentAuthorizationViewControllerDelegate {
130
164
  private weak var paymentHandler: HybridPaymentHandler?
131
165
  private var paymentAuthorized: Bool = false
166
+ private var presentationDate: Date?
167
+ private let userDismissThreshold: TimeInterval = 0.75
132
168
 
133
169
  init(paymentHandler: HybridPaymentHandler) {
134
170
  self.paymentHandler = paymentHandler
135
171
  }
172
+
173
+ func markPresented() {
174
+ presentationDate = Date()
175
+ }
136
176
 
137
177
  func paymentAuthorizationViewController(
138
178
  _ controller: PKPaymentAuthorizationViewController,
@@ -163,11 +203,19 @@ private class PaymentDelegate: NSObject, PKPaymentAuthorizationViewControllerDel
163
203
  func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
164
204
  controller.dismiss(animated: true) {
165
205
  if !self.paymentAuthorized {
206
+ let errorMessage: String
207
+ if let presentationDate = self.presentationDate,
208
+ Date().timeIntervalSince(presentationDate) >= self.userDismissThreshold {
209
+ errorMessage = ErrorMessage.paymentCancelled
210
+ } else {
211
+ errorMessage = ErrorMessage.paymentDismissed
212
+ }
213
+
166
214
  let result = PaymentResult.init(
167
215
  success: false,
168
216
  transactionId: nil,
169
217
  token: nil,
170
- error: ErrorMessage.paymentCancelled
218
+ error: errorMessage
171
219
  )
172
220
  self.paymentHandler?.handlePaymentResult(result)
173
221
  }
@@ -216,7 +264,10 @@ class HybridPaymentHandler: HybridPaymentHandlerSpec {
216
264
  // MARK: - Private Methods
217
265
 
218
266
  private func performPayment(request: PaymentRequest, completion: @escaping (PaymentResult) -> Void) {
219
- let paymentRequest = PaymentRequestBuilder.build(from: request)
267
+ guard let paymentRequest = PaymentRequestBuilder.build(from: request) else {
268
+ completion(createErrorResult(ErrorMessage.missingMerchantIdentifier))
269
+ return
270
+ }
220
271
 
221
272
  paymentCompletion = completion
222
273
  currentPaymentRequest = paymentRequest
@@ -229,12 +280,14 @@ class HybridPaymentHandler: HybridPaymentHandlerSpec {
229
280
 
230
281
  paymentAuthVC.delegate = delegate
231
282
 
232
- guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
283
+ guard let rootViewController = getRootViewController() else {
233
284
  completion(createErrorResult(ErrorMessage.unableToPresent))
234
285
  return
235
286
  }
236
287
 
237
- rootViewController.present(paymentAuthVC, animated: true)
288
+ rootViewController.present(paymentAuthVC, animated: true) {
289
+ self.delegate?.markPresented()
290
+ }
238
291
  }
239
292
 
240
293
  private func createErrorResult(_ error: String) -> PaymentResult {
@@ -245,4 +298,35 @@ class HybridPaymentHandler: HybridPaymentHandlerSpec {
245
298
  error: error
246
299
  )
247
300
  }
301
+
302
+ private func getRootViewController() -> UIViewController? {
303
+ let scenes = UIApplication.shared.connectedScenes
304
+ .compactMap { $0 as? UIWindowScene }
305
+ .filter { $0.activationState == .foregroundActive }
306
+
307
+ let rootViewController = scenes
308
+ .flatMap(\.windows)
309
+ .first(where: \.isKeyWindow)?
310
+ .rootViewController
311
+
312
+ return topViewController(from: rootViewController)
313
+ }
314
+
315
+ private func topViewController(from viewController: UIViewController?) -> UIViewController? {
316
+ guard let viewController else { return nil }
317
+
318
+ if let navigationController = viewController as? UINavigationController {
319
+ return topViewController(from: navigationController.visibleViewController)
320
+ }
321
+
322
+ if let tabBarController = viewController as? UITabBarController {
323
+ return topViewController(from: tabBarController.selectedViewController)
324
+ }
325
+
326
+ if let presentedViewController = viewController.presentedViewController {
327
+ return topViewController(from: presentedViewController)
328
+ }
329
+
330
+ return viewController
331
+ }
248
332
  }
@@ -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
@@ -14,7 +14,8 @@ const utils_1 = require("../utils");
14
14
  * - Payment processing
15
15
  * - State management
16
16
  *
17
- * @param config - Payment configuration
17
+ * @param config - Hook configuration including locale, supported networks,
18
+ * Apple Pay override, and Google Pay gateway settings.
18
19
  * @returns Complete payment checkout interface
19
20
  *
20
21
  * @example
@@ -31,9 +32,9 @@ const utils_1 = require("../utils");
31
32
  * isProcessing,
32
33
  * error,
33
34
  * } = usePaymentCheckout({
34
- * merchantIdentifier: 'merchant.com.example',
35
35
  * currencyCode: 'USD',
36
36
  * countryCode: 'US',
37
+ * googlePayMerchantId: 'your_google_pay_merchant_id',
37
38
  * })
38
39
  *
39
40
  * // Add single item
@@ -88,15 +89,16 @@ function usePaymentCheckout(config) {
88
89
  }, []);
89
90
  const total = (0, react_1.useMemo)(() => (0, utils_1.calculateTotal)(items), [items]);
90
91
  const paymentRequest = (0, react_1.useMemo)(() => {
91
- const { merchantIdentifier, merchantName, countryCode = 'US', currencyCode = 'USD', supportedNetworks = ['visa', 'mastercard', 'amex', 'discover'], merchantCapabilities = ['3DS'], googlePayEnvironment, googlePayGateway, googlePayGatewayMerchantId, } = config;
92
+ const { merchantName, countryCode = 'US', currencyCode = 'USD', supportedNetworks = ['visa', 'mastercard', 'amex', 'discover'], merchantCapabilities = ['3DS'], applePayMerchantIdentifier, googlePayMerchantId, googlePayEnvironment, googlePayGateway, googlePayGatewayMerchantId, } = config;
92
93
  return {
93
- merchantIdentifier,
94
+ applePayMerchantIdentifier,
94
95
  countryCode,
95
96
  merchantName,
96
97
  currencyCode,
97
98
  supportedNetworks,
98
99
  merchantCapabilities,
99
100
  paymentItems: items.length > 0 ? items : [(0, utils_1.createPaymentItem)('Total', 0, 'final')],
101
+ googlePayMerchantId,
100
102
  googlePayEnvironment,
101
103
  googlePayGateway,
102
104
  googlePayGatewayMerchantId,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const mockWithGooglePay = jest.fn((config, _props) => ({
7
+ ...config,
8
+ googlePay: true,
9
+ }));
10
+ const mockWithApplePay = jest.fn((config, _props) => ({
11
+ ...config,
12
+ applePay: true,
13
+ }));
14
+ jest.mock('../withGooglePay', () => ({
15
+ withGooglePay: (config, props) => mockWithGooglePay(config, props),
16
+ }));
17
+ jest.mock('../withApplePay', () => ({
18
+ withApplePay: (config, props) => mockWithApplePay(config, props),
19
+ }));
20
+ const index_1 = __importDefault(require("../index"));
21
+ describe('withReactNativePay', () => {
22
+ it('applies Google Pay and Apple Pay plugins in order', () => {
23
+ const inputConfig = { name: 'my-app' };
24
+ const props = {
25
+ merchantIdentifier: 'merchant.com.test',
26
+ enableGooglePay: true,
27
+ };
28
+ const result = (0, index_1.default)(inputConfig, props);
29
+ expect(mockWithGooglePay).toHaveBeenCalledWith(inputConfig, props);
30
+ expect(mockWithApplePay).toHaveBeenCalledWith({ name: 'my-app', googlePay: true }, props);
31
+ expect(result).toEqual({ name: 'my-app', googlePay: true, applePay: true });
32
+ });
33
+ });
@@ -0,0 +1 @@
1
+ export {};