react-amwal-pay 0.1.16 → 0.1.18

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.
@@ -14,10 +14,11 @@ Pod::Spec.new do |s|
14
14
  s.source = { :git => "https://github.com/amwal-pay/AnwalPaySDKReactNative.git", :tag => "#{s.version}" }
15
15
 
16
16
  s.source_files = "ios/**/*.{h,m,mm,swift}"
17
+ s.exclude_files = "ios/build/**"
17
18
  s.private_header_files = "ios/**/*.h"
18
19
 
19
20
  # Default to Release subspec
20
21
  amwal_subspec = ENV['AMWAL_SUBSPEC'] || 'Release'
21
- s.dependency "amwalsdk/#{amwal_subspec}"
22
+ s.dependency "amwalsdk/#{amwal_subspec}", '>= 1.1.93'
22
23
  install_modules_dependencies(s)
23
24
  end
@@ -79,7 +79,7 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
79
79
  dependencies {
80
80
  implementation "com.facebook.react:react-android"
81
81
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
82
- implementation("com.amwal-pay:amwal_sdk:+"){
82
+ implementation("com.amwal-pay:amwal_sdk:1.1.90"){
83
83
  exclude group: 'com.android.support', module: 'support-v4'
84
84
  exclude group: 'com.android.support', module: 'design'
85
85
  }
@@ -1,4 +1,4 @@
1
- ReactAmwalPay_kotlinVersion=2.0.21
1
+ ReactAmwalPay_kotlinVersion=2.2.20
2
2
  ReactAmwalPay_minSdkVersion=24
3
3
  ReactAmwalPay_targetSdkVersion=34
4
4
  ReactAmwalPay_compileSdkVersion=35
@@ -3,394 +3,144 @@ import amwalsdk
3
3
  import React
4
4
  import UIKit
5
5
 
6
- // MARK: - Container view controller that keeps SDK alive during 3DS presentation
7
- /// This container wraps the SDK view controller as a child
8
- /// When 3DS opens its WebView, it can present on top without dismissing the SDK
9
- class ShareableContainerViewController: UIViewController {
10
- private let sdkViewController: UIViewController
11
-
12
- init(sdkViewController: UIViewController) {
13
- self.sdkViewController = sdkViewController
14
- super.init(nibName: nil, bundle: nil)
15
- }
16
-
17
- required init?(coder: NSCoder) {
18
- fatalError("init(coder:) has not been implemented")
19
- }
20
-
21
- override func viewDidLoad() {
22
- super.viewDidLoad()
23
-
24
- // Add SDK view controller as child - this keeps it alive during 3DS presentation
25
- addChild(sdkViewController)
26
- view.addSubview(sdkViewController.view)
27
- sdkViewController.view.frame = view.bounds
28
- sdkViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
29
- sdkViewController.didMove(toParent: self)
30
-
31
- view.backgroundColor = .clear
32
- sdkViewController.view.backgroundColor = .clear
33
-
34
- // Enable presentation context to handle 3DS WebView presentation
35
- // This is critical: allows 3DS WebView to present on top without dismissing the SDK
36
- definesPresentationContext = true
37
- providesPresentationContextTransitionStyle = true
38
-
39
- // Ensure the SDK view controller also allows nested presentations
40
- // This prevents it from being dismissed when 3DS presents its WebView
41
- sdkViewController.definesPresentationContext = true
42
- sdkViewController.providesPresentationContextTransitionStyle = true
43
- }
44
-
45
- override func viewDidAppear(_ animated: Bool) {
46
- super.viewDidAppear(animated)
47
- // Ensure container is ready for nested presentations
48
- }
49
-
50
- private func topmostPresentedViewController() -> UIViewController {
51
- var topController: UIViewController = self
52
- while let presented = topController.presentedViewController {
53
- topController = presented
54
- }
55
- return topController
56
- }
57
-
58
- override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
59
- // If already presenting, present from the topmost controller
60
- // This allows 3DS WebView to present on top without dismissing the SDK
61
- if let presented = presentedViewController {
62
- let topmost = topmostPresentedViewController()
63
- topmost.present(viewControllerToPresent, animated: flag, completion: completion)
64
- } else {
65
- super.present(viewControllerToPresent, animated: flag, completion: completion)
66
- }
67
- }
6
+ struct AmwalLog {
7
+ private static let prefix = "[AMWAL_PAY_SDK]"
8
+ static func debug(_ msg: String, tag: String = "") { print("\(prefix) 🔵 [\(tag)] \(msg)") }
9
+ static func info(_ msg: String, tag: String = "") { print("\(prefix) 🟢 [\(tag)] \(msg)") }
10
+ static func warn(_ msg: String, tag: String = "") { print("\(prefix) 🟡 [\(tag)] \(msg)") }
11
+ static func error(_ msg: String, tag: String = "") { print("\(prefix) 🔴 [\(tag)] \(msg)") }
68
12
  }
69
13
 
70
- // MARK: - Fix UIViewController presentation for share sheets
71
- public extension UIViewController {
72
- static let swizzlePresentOnce: Void = {
73
- let originalSelector = #selector(UIViewController.present(_:animated:completion:))
74
- let swizzledSelector = #selector(UIViewController.swizzled_present(_:animated:completion:))
75
-
76
- guard let originalMethod = class_getInstanceMethod(UIViewController.self, originalSelector),
77
- let swizzledMethod = class_getInstanceMethod(UIViewController.self, swizzledSelector) else {
78
- return
79
- }
80
-
81
- method_exchangeImplementations(originalMethod, swizzledMethod)
82
- }()
83
-
84
- @objc dynamic func swizzled_present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
85
- // Check if this view controller's view is in the window hierarchy
86
- if self.view.window == nil {
87
- print("⚠️ Attempting to present on VC not in hierarchy, finding correct presenter...")
88
- // Find the correct view controller to present from
89
- if let topVC = UIViewController.getTopMostViewController() {
90
- topVC.swizzled_present(viewControllerToPresent, animated: flag, completion: completion)
91
- return
92
- }
93
- }
94
-
95
- // Special handling for ShareableContainerViewController and its children
96
- // If presenting from a child of ShareableContainerViewController, forward to container
97
- if let container = findShareableContainer() {
98
- if container != self {
99
- // We're a child of the container, forward to container
100
- container.swizzled_present(viewControllerToPresent, animated: flag, completion: completion)
101
- return
102
- }
103
- // We ARE the container - allow nested presentations
104
- // Don't auto-dismiss, just present on top
105
- } else if self.presentedViewController != nil && !self.definesPresentationContext {
106
- // Not a container, and already presenting - auto-dismiss
107
- print("⚠️ Already presenting, dismissing first...")
108
- self.dismiss(animated: false) { [weak self] in
109
- self?.swizzled_present(viewControllerToPresent, animated: flag, completion: completion)
110
- }
111
- return
112
- }
113
-
114
- // Call original implementation
115
- self.swizzled_present(viewControllerToPresent, animated: flag, completion: completion)
116
- }
117
-
118
- // Helper to find ShareableContainerViewController in parent hierarchy
119
- private func findShareableContainer() -> ShareableContainerViewController? {
120
- var current: UIViewController? = self
121
- while let parent = current?.parent {
122
- if let container = parent as? ShareableContainerViewController {
123
- return container
124
- }
125
- current = parent
126
- }
127
- if let container = self as? ShareableContainerViewController {
128
- return container
129
- }
130
- return nil
131
- }
132
-
14
+ extension UIViewController {
133
15
  static func getTopMostViewController() -> UIViewController? {
134
- var topController: UIViewController?
135
-
136
- if #available(iOS 13.0, *) {
137
- let keyWindow = UIApplication.shared.connectedScenes
138
- .compactMap { $0 as? UIWindowScene }
139
- .flatMap { $0.windows }
140
- .first { $0.isKeyWindow }
141
- topController = keyWindow?.rootViewController
142
- } else {
143
- topController = UIApplication.shared.keyWindow?.rootViewController
144
- }
145
-
146
- while let presented = topController?.presentedViewController {
147
- topController = presented
16
+ let keyWindow = UIApplication.shared.connectedScenes
17
+ .compactMap { $0 as? UIWindowScene }
18
+ .flatMap { $0.windows }
19
+ .first { $0.isKeyWindow }
20
+ var top = keyWindow?.rootViewController
21
+ while let presented = top?.presentedViewController {
22
+ top = presented
148
23
  }
149
-
150
- return topController
24
+ return top
151
25
  }
152
26
  }
153
27
 
154
28
  @objc(ReactAmwalPay)
155
29
  open class ReactAmwalPay: RCTEventEmitter {
156
30
  private var hasListeners = false
157
- private var sdkWindow: UIWindow? // Separate window for SDK to avoid modal dismissal issues
158
-
159
- // Initialize swizzling when the class is first loaded
160
- private static let initializeSwizzling: Void = {
161
- _ = UIViewController.swizzlePresentOnce
162
- }()
163
-
164
- // Initialize swizzling when the module is loaded
165
- public override init() {
166
- super.init()
167
- _ = ReactAmwalPay.initializeSwizzling
168
- }
31
+ private weak var presentingVC: UIViewController?
169
32
 
170
33
  open override func supportedEvents() -> [String]! {
171
34
  return ["onResponse", "onCustomerId"]
172
35
  }
173
36
 
174
- open override func startObserving() {
175
- print("🔴 startObserving called - setting hasListeners = true")
176
- hasListeners = true
177
- }
178
-
179
- open override func stopObserving() {
180
- print("🔴 stopObserving called - setting hasListeners = false")
181
- hasListeners = false
182
- }
37
+ open override func startObserving() { hasListeners = true }
38
+ open override func stopObserving() { hasListeners = false }
183
39
 
184
40
  private func emitOnResponse(_ params: [String: Any]) {
185
- print("🔴 emitOnResponse called with params: \(params)")
186
- print("🔴 hasListeners: \(hasListeners)")
187
- if hasListeners {
188
- print("🔴 Sending onResponse event to JS")
189
- sendEvent(withName: "onResponse", body: params)
190
- } else {
191
- print("🔴 NOT sending - no listeners!")
41
+ guard hasListeners else {
42
+ AmwalLog.warn("onResponse not sent — no listeners", tag: "EVENTS")
43
+ return
192
44
  }
45
+ sendEvent(withName: "onResponse", body: params)
193
46
  }
194
47
 
195
48
  private func emitOnCustomerId(_ customerId: String?) {
196
- print("🔴 emitOnCustomerId called with: \(customerId ?? "nil")")
197
- print("🔴 hasListeners: \(hasListeners)")
198
- if hasListeners {
199
- print("🔴 Sending onCustomerId event to JS")
200
- sendEvent(withName: "onCustomerId", body: customerId)
201
- } else {
202
- print("🔴 NOT sending - no listeners!")
203
- }
49
+ guard hasListeners else { return }
50
+ sendEvent(withName: "onCustomerId", body: customerId)
204
51
  }
205
52
 
206
- private func mapEnvironment(environment: String) -> Config.Environment {
207
- switch environment {
208
- case "PROD": return .PROD
209
- case "UAT": return .UAT
210
- case "SIT": return .SIT
211
- default: return .PROD
212
- }
53
+ private func mapEnvironment(_ s: String) -> Config.Environment {
54
+ switch s { case "PROD": return .PROD; case "UAT": return .UAT; default: return .SIT }
213
55
  }
214
-
215
- private func mapCurrency(currency: String) -> Config.Currency {
216
- switch currency {
217
- case "OMR": return .OMR
218
- default: return .OMR
219
- }
56
+ private func mapCurrency(_ s: String) -> Config.Currency { return .OMR }
57
+ private func mapTransactionType(_ s: String) -> Config.TransactionType {
58
+ switch s { case "NFC": return .nfc; case "APPLE_PAY": return .applePay; default: return .cardWallet }
220
59
  }
221
-
222
- private func mapTransactionType(transactionType: String) -> Config.TransactionType {
223
- switch transactionType {
224
- case "CARD_WALLET": return .cardWallet
225
- case "NFC": return .nfc
226
- case "APPLE_PAY": return .applePay
227
- default: return .cardWallet
228
- }
60
+ private func mapLocale(_ s: String) -> Config.Locale {
61
+ switch s { case "ar": return .ar; default: return .en }
229
62
  }
230
-
231
- private func mapLocale(locale: String) -> Config.Locale {
232
- switch locale {
233
- case "en": return .en
234
- case "ar": return .ar
235
- default: return .en
236
- }
237
- }
238
-
239
- private func prepareConfig(config: [String: Any]) -> Config {
240
- // Handle additionValues
241
- var additionValues: [String: String] = Config.generateDefaultAdditionValues()
242
- if let configAdditionValues = config["additionValues"] as? [String: String] {
243
- // Merge with default values, allowing override
244
- for (key, value) in configAdditionValues {
245
- additionValues[key] = value
246
- }
247
- }
248
63
 
64
+ private func buildConfig(_ raw: [String: Any]) -> Config {
65
+ var additionValues = Config.generateDefaultAdditionValues()
66
+ if let extra = raw["additionValues"] as? [String: String] {
67
+ extra.forEach { additionValues[$0] = $1 }
68
+ }
69
+ let secureHash = raw["secureHash"] as? String
249
70
  return Config(
250
- environment: mapEnvironment(environment: config["environment"] as? String ?? "PROD"),
251
- sessionToken: config["sessionToken"] as? String ?? "",
252
- currency: mapCurrency(currency: config["currency"] as? String ?? "OMR"),
253
- amount: config["amount"] as? String ?? "",
254
- merchantId: config["merchantId"] as? String ?? "",
255
- terminalId: config["terminalId"] as? String ?? "",
256
- customerId: config["customerId"] as? String,
257
- locale: mapLocale(locale: config["locale"] as? String ?? "en"),
258
- transactionType: mapTransactionType(transactionType: config["transactionType"] as? String ?? "CARD_WALLET"),
259
- transactionId: config["transactionId"] as? String ?? Config.generateTransactionId(),
71
+ environment: mapEnvironment(raw["environment"] as? String ?? "SIT"),
72
+ sessionToken: raw["sessionToken"] as? String ?? "",
73
+ currency: mapCurrency(raw["currency"] as? String ?? "OMR"),
74
+ amount: raw["amount"] as? String ?? "",
75
+ merchantId: raw["merchantId"] as? String ?? "",
76
+ terminalId: raw["terminalId"] as? String ?? "",
77
+ customerId: raw["customerId"] as? String,
78
+ locale: mapLocale(raw["locale"] as? String ?? "en"),
79
+ transactionType: mapTransactionType(raw["transactionType"] as? String ?? "CARD_WALLET"),
80
+ transactionId: raw["transactionId"] as? String ?? Config.generateTransactionId(),
260
81
  additionValues: additionValues,
261
- merchantReference: config["merchantReference"] as? String
82
+ merchantReference: raw["merchantReference"] as? String,
83
+ secureHash: secureHash
262
84
  )
263
85
  }
264
-
86
+
265
87
  @objc
266
88
  open func initiate(_ config: [String: Any]) {
267
- DispatchQueue.main.async {
268
- do {
269
- let sdkConfig = self.prepareConfig(config: config)
270
- let sdk = AmwalSDK()
271
-
272
- guard let rootVC = UIApplication.shared.keyWindow?.rootViewController else {
273
- let errorData: [String: Any] = [
274
- "data": [
275
- "status": "ERROR",
276
- "message": "No root view controller found"
277
- ]
278
- ]
279
- self.emitOnResponse(errorData)
280
- return
281
- }
282
-
283
- let sdkVC = try sdk.createViewController(
284
- config: sdkConfig,
285
- onResponse: { [weak self] response in
286
- print("🟠 SDK onResponse callback fired!")
287
- print("🟠 Response type: \(type(of: response))")
288
- print("🟠 Response value: \(response ?? "nil")")
289
-
290
- // Dismiss SDK window when payment completes
291
- DispatchQueue.main.async {
292
- self?.dismissSDKWindow()
293
- }
294
-
295
- // The SDK returns a JSON string, we need to parse it
296
- if let responseString = response as? String,
297
- let data = responseString.data(using: .utf8),
298
- let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
299
- print("🟠 Successfully parsed JSON string to dictionary")
300
- self?.emitOnResponse(json)
301
- } else if let responseDict = response as? [String: Any] {
302
- print("🟠 Response is already a dictionary")
303
- self?.emitOnResponse(responseDict)
304
- } else {
305
- print("🟠 Could not parse response, sending as-is")
306
- let errorData: [String: Any] = [
307
- "status": "error",
308
- "message": "Invalid response format",
309
- "rawResponse": String(describing: response)
310
- ]
311
- self?.emitOnResponse(errorData)
312
- }
313
- },
314
- onCustomerId: { [weak self] customerId in
315
- print("🟠 SDK onCustomerId callback fired with: \(customerId ?? "nil")")
316
- self?.emitOnCustomerId(customerId)
317
- }
318
- )
319
-
320
- // Ensure transparency
321
- sdkVC.view.backgroundColor = .clear
322
- sdkVC.view.isOpaque = false
323
-
324
- // Wrap SDK view controller in container
325
- let containerVC = ShareableContainerViewController(sdkViewController: sdkVC)
326
- containerVC.view.backgroundColor = .clear
327
-
328
- print("🟠 SDK ViewController wrapped in container")
329
- print("🟠 Creating separate window for SDK...")
330
-
331
- // Create a separate window for the SDK
332
- // This prevents modal presentation issues - SDK lives in its own window
333
- // 3DS can present on top without affecting the SDK
334
- if #available(iOS 13.0, *) {
335
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
336
- self.sdkWindow = UIWindow(windowScene: windowScene)
337
- }
338
- } else {
339
- self.sdkWindow = UIWindow(frame: UIScreen.main.bounds)
340
- }
341
-
342
- guard let sdkWindow = self.sdkWindow else {
343
- print("🟠 Failed to create SDK window, falling back to modal presentation")
344
- rootVC.present(containerVC, animated: true)
345
- return
346
- }
347
-
348
- sdkWindow.rootViewController = containerVC
349
- sdkWindow.windowLevel = .normal + 1 // Above main window
350
- sdkWindow.backgroundColor = .clear
351
- sdkWindow.isOpaque = false
352
- sdkWindow.makeKeyAndVisible()
353
-
354
- print("🟠 SDK window created and made visible")
355
- } catch {
356
- print("Presentation failed: \(error.localizedDescription)")
357
- let errorData: [String: Any] = [
358
- "data": [
359
- "status": "ERROR",
360
- "message": error.localizedDescription
361
- ]
362
- ]
363
- self.emitOnResponse(errorData)
364
- }
365
- }
89
+ DispatchQueue.main.async {
90
+ do {
91
+ let sdkConfig = self.buildConfig(config)
92
+ AmwalLog.info("Building SDK config — env: \(sdkConfig.environment), amount: \(sdkConfig.amount)", tag: "SDK")
93
+
94
+ guard let rootVC = UIViewController.getTopMostViewController() else {
95
+ AmwalLog.error("No root VC found", tag: "SDK")
96
+ self.emitOnResponse(["status": "ERROR", "message": "No root view controller"])
97
+ return
98
+ }
99
+ AmwalLog.info("Presenting from: \(type(of: rootVC))", tag: "SDK")
100
+
101
+ let sdk = AmwalSDK()
102
+ let sdkVC = try sdk.createViewController(
103
+ config: sdkConfig,
104
+ onResponse: { [weak self] response in
105
+ AmwalLog.info("onResponse received", tag: "SDK")
106
+ DispatchQueue.main.async {
107
+ self?.presentingVC?.dismiss(animated: true)
108
+ self?.presentingVC = nil
109
+ }
110
+ guard let response = response,
111
+ let data = response.data(using: .utf8),
112
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
113
+ else { return }
114
+ self?.emitOnResponse(json)
115
+ },
116
+ onCustomerId: { [weak self] customerId in
117
+ self?.emitOnCustomerId(customerId)
118
+ }
119
+ )
120
+
121
+ sdkVC.modalPresentationStyle = .overFullScreen
122
+ self.presentingVC = sdkVC
123
+ rootVC.present(sdkVC, animated: true) {
124
+ AmwalLog.info("SDK presented", tag: "SDK")
125
+ }
126
+ } catch {
127
+ AmwalLog.error("initiate failed: \(error)", tag: "SDK")
128
+ self.emitOnResponse(["status": "ERROR", "message": error.localizedDescription])
129
+ }
130
+ }
366
131
  }
367
-
132
+
368
133
  @objc
369
134
  open override func addListener(_ eventName: String) {
370
135
  super.addListener(eventName)
371
- print("🔴 addListener called for: \(eventName)")
372
- if !hasListeners {
373
- hasListeners = true
374
- print("🔴 Set hasListeners = true")
375
- }
136
+ hasListeners = true
376
137
  }
377
138
 
378
139
  @objc
379
140
  open override func removeListeners(_ count: Double) {
380
141
  super.removeListeners(count)
381
- print("🔴 removeListeners called with count: \(count)")
382
142
  }
383
143
 
384
144
  @objc
385
- public override static func requiresMainQueueSetup() -> Bool {
386
- return true
387
- }
388
-
389
- // Dismiss SDK window when payment completes
390
- private func dismissSDKWindow() {
391
- print("🟠 Dismissing SDK window...")
392
- sdkWindow?.isHidden = true
393
- sdkWindow?.rootViewController = nil
394
- sdkWindow = nil
395
- }
145
+ public override static func requiresMainQueueSetup() -> Bool { return true }
396
146
  }