@jimrising/easymerchantsdk-react-native 2.5.2 → 2.5.4

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.
@@ -96,7 +96,7 @@ dependencies {
96
96
  implementation 'com.google.android.material:material:1.13.0'
97
97
 
98
98
  // Third-party libs
99
- implementation 'com.app:paysdk:1.6.6.3'
99
+ implementation 'com.app:paysdk:1.7.0'
100
100
  implementation 'com.hbb20:ccp:2.7.3'
101
101
  implementation 'com.github.bumptech.glide:glide:5.0.4'
102
102
  implementation 'com.github.androidmads:QRGenerator:1.0.5'
@@ -22,12 +22,6 @@ struct APIHeaders {
22
22
  return ["Customer-Token": token]
23
23
  }
24
24
 
25
- /// For verify_otp only: match Android — header "Customer-Token" (Pascal case), no Client-Token, no X-Api-Key/Secret
26
- static func verifyOtpHeaders() -> [String: String] {
27
- guard let token = UserStoreSingleton.shared.customerToken else { return [:] }
28
- return ["Customer-Token": token]
29
- }
30
-
31
25
  /// API key/secret headers (match Android SDK: X-Api-Key, X-Api-Secret)
32
26
  static func apiAuthHeaders() -> [String: String] {
33
27
  guard let key = EnvironmentConfig.apiKey,
@@ -29,14 +29,9 @@ struct APIRequest {
29
29
 
30
30
  // Combine default headers with passed-in headers
31
31
  var allHeaders = APIHeaders.commonHeaders()
32
- if endpoint == .verifyOtp {
33
- // verify_otp: match Android — only Customer-Token + Content-Type (no Client-Token, no X-Api-Key/Secret)
34
- allHeaders.merge(APIHeaders.verifyOtpHeaders(), uniquingKeysWith: { $1 })
35
- } else {
36
- allHeaders.merge(APIHeaders.clientTokenHeader(), uniquingKeysWith: { $1 })
37
- allHeaders.merge(APIHeaders.customerTokenHeader(), uniquingKeysWith: { $1 })
38
- allHeaders.merge(APIHeaders.apiAuthHeaders(), uniquingKeysWith: { $1 })
39
- }
32
+ allHeaders.merge(APIHeaders.clientTokenHeader(), uniquingKeysWith: { $1 })
33
+ allHeaders.merge(APIHeaders.customerTokenHeader(), uniquingKeysWith: { $1 })
34
+ allHeaders.merge(APIHeaders.apiAuthHeaders(), uniquingKeysWith: { $1 })
40
35
  allHeaders.merge(headers, uniquingKeysWith: { $1 }) // override if needed
41
36
 
42
37
  // Apply to request
@@ -2,8 +2,8 @@
2
2
  #import <React/RCTLog.h>
3
3
  #import <React/RCTBridgeModule.h>
4
4
 
5
- #import "easymerchantsdk-Swift.h"
6
- //#import <easymerchantsdk/easymerchantsdk-Swift.h>
5
+ //#import "easymerchantsdk-Swift.h"
6
+ #import <easymerchantsdk/easymerchantsdk-Swift.h>
7
7
 
8
8
 
9
9
  @interface EasyMerchantSdk ()
@@ -84,8 +84,8 @@ public class EasyMerchantSdkPlugin: NSObject, RCTBridgeModule {
84
84
  return
85
85
  }
86
86
 
87
- guard let presentingVC = viewController ?? getTopViewController() else {
88
- reject("NO_VIEW_CONTROLLER", "Unable to find a valid view controller", nil)
87
+ guard let (presentingVC, toDismiss) = presentingViewControllerAndOptionalDismiss() else {
88
+ reject("NO_VIEW_CONTROLLER", "Unable to find a view controller in the window hierarchy", nil)
89
89
  clearCallbacks()
90
90
  return
91
91
  }
@@ -98,8 +98,8 @@ public class EasyMerchantSdkPlugin: NSObject, RCTBridgeModule {
98
98
  }
99
99
 
100
100
  let controller = EasyPayViewController(request: request, delegate: self)
101
- DispatchQueue.main.async {
102
- presentingVC.present(controller, animated: true)
101
+ DispatchQueue.main.async { [weak self] in
102
+ self?.presentPaymentController(controller, from: presentingVC, dismissFirst: toDismiss)
103
103
  }
104
104
  }
105
105
 
@@ -117,15 +117,15 @@ public class EasyMerchantSdkPlugin: NSObject, RCTBridgeModule {
117
117
  return
118
118
  }
119
119
 
120
- guard let presentingVC = viewController ?? getTopViewController() else {
121
- reject("NO_VIEW_CONTROLLER", "Unable to find a valid view controller", nil)
120
+ guard let (presentingVC, toDismiss) = presentingViewControllerAndOptionalDismiss() else {
121
+ reject("NO_VIEW_CONTROLLER", "Unable to find a view controller in the window hierarchy", nil)
122
122
  clearCallbacks()
123
123
  return
124
124
  }
125
125
 
126
126
  let controller = EasyPayViewController(request: request, delegate: self)
127
- DispatchQueue.main.async {
128
- presentingVC.present(controller, animated: true)
127
+ DispatchQueue.main.async { [weak self] in
128
+ self?.presentPaymentController(controller, from: presentingVC, dismissFirst: toDismiss)
129
129
  }
130
130
  }
131
131
 
@@ -145,16 +145,75 @@ public class EasyMerchantSdkPlugin: NSObject, RCTBridgeModule {
145
145
 
146
146
  // MARK: - Helpers
147
147
 
148
- private func getTopViewController() -> UIViewController? {
149
- guard let windowScene = UIApplication.shared.connectedScenes
150
- .filter({ $0.activationState == .foregroundActive })
151
- .first as? UIWindowScene,
152
- let rootVC = windowScene.windows
153
- .filter({ $0.isKeyWindow })
154
- .first?.rootViewController else {
155
- return nil
148
+ private func keyWindowRootViewController() -> UIViewController? {
149
+ if let windowScene = UIApplication.shared.connectedScenes
150
+ .first(where: { ($0 as? UIWindowScene)?.activationState == .foregroundActive }) as? UIWindowScene,
151
+ let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }),
152
+ let root = keyWindow.rootViewController {
153
+ return root
156
154
  }
157
-
155
+ return UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController
156
+ }
157
+
158
+ /// Returns (presenter, viewControllerToDismiss). If the top is a modal host or our EasyPay, we must dismiss it first so presenter is not "already presenting".
159
+ private func presentingViewControllerAndOptionalDismiss() -> (UIViewController, UIViewController?)? {
160
+ guard var vc = keyWindowRootViewController() else { return nil }
161
+ while let presented = vc.presentedViewController {
162
+ vc = presented
163
+ }
164
+ let top = vc
165
+ // Step back from modal host or our own payment VC to get the real presenter
166
+ while (isModalHostViewController(vc) || isEasyPayViewController(vc)), let presenter = vc.presentingViewController {
167
+ vc = presenter
168
+ }
169
+ if isModalHostViewController(vc) || isEasyPayViewController(vc) { return nil }
170
+ // If top was a modal or EasyPay, that top is currently presented by vc; we must dismiss it first
171
+ let dismissFirst: UIViewController? = (top !== vc) ? top : nil
172
+ return (vc, dismissFirst)
173
+ }
174
+
175
+ private func presentPaymentController(_ controller: UIViewController, from presentingVC: UIViewController, dismissFirst: UIViewController?) {
176
+ if let toDismiss = dismissFirst {
177
+ toDismiss.dismiss(animated: false) { [weak self] in
178
+ DispatchQueue.main.async {
179
+ // After dismiss, get top then step back from modal host / EasyPay so we never present from them
180
+ guard let presenter = self?.presentingViewControllerInWindowHierarchy() else {
181
+ presentingVC.present(controller, animated: true)
182
+ return
183
+ }
184
+ presenter.present(controller, animated: true)
185
+ }
186
+ }
187
+ } else {
188
+ presentingVC.present(controller, animated: true)
189
+ }
190
+ }
191
+
192
+ /// Present from a VC that can safely present (not RCTFabricModalHostViewController; not our EasyPayViewController).
193
+ private func presentingViewControllerInWindowHierarchy() -> UIViewController? {
194
+ guard var vc = keyWindowRootViewController() else { return nil }
195
+ while let presented = vc.presentedViewController {
196
+ vc = presented
197
+ }
198
+ while (isModalHostViewController(vc) || isEasyPayViewController(vc)), let presenter = vc.presentingViewController {
199
+ vc = presenter
200
+ }
201
+ if isModalHostViewController(vc) || isEasyPayViewController(vc) { return nil }
202
+ return vc
203
+ }
204
+
205
+ private func isModalHostViewController(_ vc: UIViewController?) -> Bool {
206
+ guard let vc = vc else { return false }
207
+ let name = String(describing: type(of: vc))
208
+ return name.contains("ModalHost") || name.contains("RCTModalHost")
209
+ }
210
+
211
+ private func isEasyPayViewController(_ vc: UIViewController?) -> Bool {
212
+ vc is EasyPayViewController
213
+ }
214
+
215
+ private func getTopViewController() -> UIViewController? {
216
+ guard let rootVC = keyWindowRootViewController() else { return nil }
158
217
  var topVC = rootVC
159
218
  while let presentedVC = topVC.presentedViewController {
160
219
  topVC = presentedVC
@@ -263,6 +263,7 @@ import UIKit
263
263
  storedCompletionHandler?(result)
264
264
 
265
265
  default:
266
+ break
266
267
  }
267
268
  }
268
269
  }
@@ -939,11 +939,11 @@ public final class Request: NSObject {
939
939
  let token = clientToken ?? UserStoreSingleton.shared.clientToken ?? ""
940
940
  uRLRequest.addValue(token, forHTTPHeaderField: "clientToken")
941
941
 
942
- // Add API key/secret headers
942
+ // Add API key/secret headers (match Android SDK: X-Api-Key, X-Api-Secret)
943
943
  if let apiKey = EnvironmentConfig.apiKey,
944
944
  let apiSecret = EnvironmentConfig.apiSecret {
945
- uRLRequest.addValue(apiKey, forHTTPHeaderField: "x-api-key")
946
- uRLRequest.addValue(apiSecret, forHTTPHeaderField: "x-api-secret")
945
+ uRLRequest.addValue(apiKey, forHTTPHeaderField: "X-Api-Key")
946
+ uRLRequest.addValue(apiSecret, forHTTPHeaderField: "X-Api-Secret")
947
947
  }
948
948
 
949
949
  let task = URLSession.shared.dataTask(with: uRLRequest) { data, response, error in