react-amwal-pay 0.1.17 → 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,397 +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
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
144
23
  }
145
-
146
- while let presented = topController?.presentedViewController {
147
- topController = presented
148
- }
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
- }
229
- }
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
- }
60
+ private func mapLocale(_ s: String) -> Config.Locale {
61
+ switch s { case "ar": return .ar; default: return .en }
237
62
  }
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,
262
- secureHash: config["secureHash"] as? String
82
+ merchantReference: raw["merchantReference"] as? String,
83
+ secureHash: secureHash
263
84
  )
264
85
  }
265
-
86
+
266
87
  @objc
267
88
  open func initiate(_ config: [String: Any]) {
268
- DispatchQueue.main.async {
269
- do {
270
- let sdkConfig = self.prepareConfig(config: config)
271
- let sdk = AmwalSDK()
272
-
273
- guard let rootVC = UIApplication.shared.keyWindow?.rootViewController else {
274
- let errorData: [String: Any] = [
275
- "data": [
276
- "status": "ERROR",
277
- "message": "No root view controller found"
278
- ]
279
- ]
280
- self.emitOnResponse(errorData)
281
- return
282
- }
283
-
284
- let sdkVC = try sdk.createViewController(
285
- config: sdkConfig,
286
- onResponse: { [weak self] response in
287
- print("🟠 SDK onResponse callback fired!")
288
- print("🟠 Response value: \(response ?? "nil")")
289
-
290
- // Always dismiss SDK window when this callback fires
291
- DispatchQueue.main.async {
292
- self?.dismissSDKWindow()
293
- }
294
-
295
- // nil response means SDK was dismissed without completing a payment
296
- // (e.g., error during initialization). Don't emit to React Native.
297
- guard let response = response else {
298
- print("🟠 nil response - SDK dismissed without payment, not emitting to JS")
299
- return
300
- }
301
-
302
- // The SDK returns a JSON string, we need to parse it
303
- if let data = response.data(using: .utf8),
304
- let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
305
- print("🟠 Successfully parsed JSON string to dictionary")
306
- self?.emitOnResponse(json)
307
- } else {
308
- print("🟠 Could not parse response as JSON, sending error")
309
- let errorData: [String: Any] = [
310
- "status": "error",
311
- "message": "Invalid response format",
312
- "rawResponse": response
313
- ]
314
- self?.emitOnResponse(errorData)
315
- }
316
- },
317
- onCustomerId: { [weak self] customerId in
318
- print("🟠 SDK onCustomerId callback fired with: \(customerId ?? "nil")")
319
- self?.emitOnCustomerId(customerId)
320
- }
321
- )
322
-
323
- // Ensure transparency
324
- sdkVC.view.backgroundColor = .clear
325
- sdkVC.view.isOpaque = false
326
-
327
- // Wrap SDK view controller in container
328
- let containerVC = ShareableContainerViewController(sdkViewController: sdkVC)
329
- containerVC.view.backgroundColor = .clear
330
-
331
- print("🟠 SDK ViewController wrapped in container")
332
- print("🟠 Creating separate window for SDK...")
333
-
334
- // Create a separate window for the SDK
335
- // This prevents modal presentation issues - SDK lives in its own window
336
- // 3DS can present on top without affecting the SDK
337
- if #available(iOS 13.0, *) {
338
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
339
- self.sdkWindow = UIWindow(windowScene: windowScene)
340
- }
341
- } else {
342
- self.sdkWindow = UIWindow(frame: UIScreen.main.bounds)
343
- }
344
-
345
- guard let sdkWindow = self.sdkWindow else {
346
- print("🟠 Failed to create SDK window, falling back to modal presentation")
347
- rootVC.present(containerVC, animated: true)
348
- return
349
- }
350
-
351
- sdkWindow.rootViewController = containerVC
352
- sdkWindow.windowLevel = .normal + 1 // Above main window
353
- sdkWindow.backgroundColor = .clear
354
- sdkWindow.isOpaque = false
355
- sdkWindow.makeKeyAndVisible()
356
-
357
- print("🟠 SDK window created and made visible")
358
- } catch {
359
- print("Presentation failed: \(error.localizedDescription)")
360
- let errorData: [String: Any] = [
361
- "data": [
362
- "status": "ERROR",
363
- "message": error.localizedDescription
364
- ]
365
- ]
366
- self.emitOnResponse(errorData)
367
- }
368
- }
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
+ }
369
131
  }
370
-
132
+
371
133
  @objc
372
134
  open override func addListener(_ eventName: String) {
373
135
  super.addListener(eventName)
374
- print("🔴 addListener called for: \(eventName)")
375
- if !hasListeners {
376
- hasListeners = true
377
- print("🔴 Set hasListeners = true")
378
- }
136
+ hasListeners = true
379
137
  }
380
138
 
381
139
  @objc
382
140
  open override func removeListeners(_ count: Double) {
383
141
  super.removeListeners(count)
384
- print("🔴 removeListeners called with count: \(count)")
385
142
  }
386
143
 
387
144
  @objc
388
- public override static func requiresMainQueueSetup() -> Bool {
389
- return true
390
- }
391
-
392
- // Dismiss SDK window when payment completes
393
- private func dismissSDKWindow() {
394
- print("🟠 Dismissing SDK window...")
395
- sdkWindow?.isHidden = true
396
- sdkWindow?.rootViewController = nil
397
- sdkWindow = nil
398
- }
145
+ public override static func requiresMainQueueSetup() -> Bool { return true }
399
146
  }