@jimrising/easymerchantsdk-react-native 2.3.9 → 2.4.1

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 (74) hide show
  1. package/.idea/caches/deviceStreaming.xml +340 -0
  2. package/.idea/em-MobileCheckoutSDK-ReactNative.iml +9 -0
  3. package/.idea/misc.xml +5 -0
  4. package/.idea/modules.xml +8 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/README.md +113 -42
  7. package/android/build/generated/source/buildConfig/debug/com/reactlibrary/BuildConfig.java +10 -0
  8. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml +7 -0
  9. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json +18 -0
  10. package/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties +6 -0
  11. package/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json +1 -0
  12. package/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar +0 -0
  13. package/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar +0 -0
  14. package/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt +0 -0
  15. package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +1 -0
  16. package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +2 -0
  17. package/android/build/intermediates/incremental/mergeDebugAssets/merger.xml +2 -0
  18. package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +2 -0
  19. package/android/build/intermediates/incremental/mergeDebugShaders/merger.xml +2 -0
  20. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/reactlibrary/BuildConfig.class +0 -0
  21. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/reactlibrary/RNEasymerchantsdkModule$1$1.class +0 -0
  22. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/reactlibrary/RNEasymerchantsdkModule$1.class +0 -0
  23. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/reactlibrary/RNEasymerchantsdkModule$2.class +0 -0
  24. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/reactlibrary/RNEasymerchantsdkModule$3.class +0 -0
  25. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/reactlibrary/RNEasymerchantsdkModule.class +0 -0
  26. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/reactlibrary/RNEasymerchantsdkPackage.class +0 -0
  27. package/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt +2 -0
  28. package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +7 -0
  29. package/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml +7 -0
  30. package/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json +1 -0
  31. package/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt +1 -0
  32. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/reactlibrary/BuildConfig.class +0 -0
  33. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/reactlibrary/RNEasymerchantsdkModule$1$1.class +0 -0
  34. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/reactlibrary/RNEasymerchantsdkModule$1.class +0 -0
  35. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/reactlibrary/RNEasymerchantsdkModule$2.class +0 -0
  36. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/reactlibrary/RNEasymerchantsdkModule$3.class +0 -0
  37. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/reactlibrary/RNEasymerchantsdkModule.class +0 -0
  38. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/reactlibrary/RNEasymerchantsdkPackage.class +0 -0
  39. package/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar +0 -0
  40. package/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt +1 -0
  41. package/android/build/outputs/logs/manifest-merger-debug-report.txt +16 -0
  42. package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
  43. package/android/build.gradle +3 -2
  44. package/ios/Classes/EasyMerchantSdk.h +4 -0
  45. package/ios/Classes/EasyMerchantSdk.m +8 -2
  46. package/ios/Classes/EasyMerchantSdk.swift +14 -0
  47. package/ios/Helper/GrailPayHelper.swift +166 -5
  48. package/ios/Pods/UserDefaults/UserStoreSingleton.swift +425 -0
  49. package/ios/Pods/ViewControllers/AdditionalInfoVC.swift +2996 -0
  50. package/ios/Pods/ViewControllers/BaseVC.swift +142 -0
  51. package/ios/Pods/ViewControllers/BillingInfoVC/BillingInfoVC.swift +3807 -0
  52. package/ios/Pods/ViewControllers/BillingInfoVC/Cells/CityListTVC.swift +46 -0
  53. package/ios/Pods/ViewControllers/BillingInfoVC/Cells/CountryListTVC.swift +47 -0
  54. package/ios/Pods/ViewControllers/BillingInfoVC/Cells/StateListTVC.swift +46 -0
  55. package/ios/Pods/ViewControllers/Clean Runner_2025-07-23T14-58-05.txt +13 -0
  56. package/ios/Pods/ViewControllers/CountryListVC.swift +435 -0
  57. package/ios/Pods/ViewControllers/EmailVerificationVC.swift +300 -0
  58. package/ios/Pods/ViewControllers/GrailPayVC.swift +492 -0
  59. package/ios/Pods/ViewControllers/OTPVerificationVC.swift +2278 -0
  60. package/ios/Pods/ViewControllers/PaymentDoneVC.swift +287 -0
  61. package/ios/Pods/ViewControllers/PaymentErrorVC.swift +85 -0
  62. package/ios/Pods/ViewControllers/PaymentInformation/AccountTypeTVC.swift +41 -0
  63. package/ios/Pods/ViewControllers/PaymentInformation/PaymentInfoVC.swift +13115 -0
  64. package/ios/Pods/ViewControllers/PaymentInformation/PaymentInformationCVC.swift +35 -0
  65. package/ios/Pods/ViewControllers/PaymentInformation/RecurringTVC.swift +40 -0
  66. package/ios/Pods/ViewControllers/PaymentInformation/SavedAccountsTVC/SavedAccountTVC.swift +80 -0
  67. package/ios/Pods/ViewControllers/PaymentInformation/SavedAccountsTVC/SavedAccountTVC.xib +163 -0
  68. package/ios/Pods/ViewControllers/PaymentInformation/SavedCardsTVC/SavedCardsTVC.swift +81 -0
  69. package/ios/Pods/ViewControllers/PaymentInformation/SavedCardsTVC/SavedCardsTVC.xib +188 -0
  70. package/ios/Pods/ViewControllers/PaymentStatusWebViewVC.swift +167 -0
  71. package/ios/Pods/ViewControllers/TermAndConditionsVC.swift +63 -0
  72. package/ios/Pods/ViewControllers/ThreeDSecurePaymentDoneVC.swift +1254 -0
  73. package/ios/easymerchantsdk.podspec +1 -1
  74. package/package.json +1 -1
@@ -0,0 +1,1254 @@
1
+ ////
2
+ //// ThreeDSecurePaymentDoneVC.swift
3
+ //// EasyPay
4
+ ////
5
+ //// Created by Mony's Mac on 20/05/25.
6
+ ////
7
+ //
8
+ //import UIKit
9
+ //
10
+ //class ThreeDSecurePaymentDoneVC: BaseVC {
11
+ //
12
+ // @IBOutlet weak var imgViewPaymentDone: UIImageView!
13
+ // @IBOutlet weak var imgViewLoading: UIImageView!
14
+ // @IBOutlet weak var viewMain: UIView!
15
+ // @IBOutlet weak var lblCompleteAuthentication: UILabel!
16
+ // @IBOutlet weak var btnDone: UIButton!
17
+ // // @IBOutlet weak var lblPaymentLink: UILabel!
18
+ // @IBOutlet weak var lblBillingInfoData: UILabel!
19
+ //
20
+ // @IBOutlet weak var viewPaymentDetails: UIStackView!
21
+ // @IBOutlet weak var lblDateHeading: UILabel!
22
+ // @IBOutlet weak var lblTransactionHeading: UILabel!
23
+ // @IBOutlet weak var lblSubscriptionHeading: UILabel!
24
+ // @IBOutlet weak var lblChargeIdHeading: UILabel!
25
+ // @IBOutlet weak var lblReferenceIdHeading: UILabel!
26
+ // @IBOutlet weak var lblPaymentMethodHeading: UILabel!
27
+ // @IBOutlet weak var lblTotalHeading: UILabel!
28
+ // @IBOutlet weak var lblStatusHeading: UILabel!
29
+ //
30
+ // @IBOutlet weak var lblDate: UILabel!
31
+ // @IBOutlet weak var lblTransactionId: UILabel!
32
+ // @IBOutlet weak var lblSubscriptionId: UILabel!
33
+ // @IBOutlet weak var lblChargeId: UILabel!
34
+ // @IBOutlet weak var lblTotal: UILabel!
35
+ // @IBOutlet weak var lblRefferenceId: UILabel!
36
+ // @IBOutlet weak var lblPaymentMethodValue: UILabel!
37
+ // @IBOutlet weak var lblStatus: UILabel!
38
+ //
39
+ // @IBOutlet weak var btnContinue: UIButton!
40
+ //
41
+ // var easyPayDelegate: EasyPayViewControllerDelegate?
42
+ // var redirectURL: String?
43
+ // var chargeData: [String: Any] = [:]
44
+ //
45
+ // var billingInfo: [String: Any]?
46
+ // var additionalInfo: [String: Any]?
47
+ //
48
+ // var threeDSecureStatusResponse: [String: Any]?
49
+ //
50
+ // // Rename for clarity, this will be for the periodic API check
51
+ // var apiStatusCheckTimer: Timer?
52
+ // // New property for the 1-minute countdown
53
+ // var oneMinuteCountdownTimer: DispatchSourceTimer?
54
+ // var countdownRemaining: Int = 180 // 120 seconds
55
+ //
56
+ // var additionalInfoData: [FieldItem]?
57
+ // var billingInfoData: [FieldItem]?
58
+ // var visibility: FieldsVisibility?
59
+ //
60
+ // // Dispatch group to wait for initial API call
61
+ // let initialAPIGroup = DispatchGroup()
62
+ //
63
+ // // var amount: Int?
64
+ // var amount: Double?
65
+ //
66
+ // var cardApiParams: [String: Any]?
67
+ //
68
+ // var request: Request!
69
+ //
70
+ // var didTapContinue = false
71
+ //
72
+ // override func viewDidLoad() {
73
+ // super.viewDidLoad()
74
+ // uiFinishingTouchElements()
75
+ //
76
+ // // setupTapOnLabel()
77
+ //
78
+ // // Initially hide done image and show loading image
79
+ // imgViewPaymentDone.isHidden = true
80
+ // imgViewLoading.isHidden = false
81
+ // lblCompleteAuthentication.text = "Completing Authentication..." // Initial text
82
+ // btnDone.isHidden = true
83
+ //
84
+ // startRotatingImage()
85
+ // startOneMinuteCountdownAndAPICheck()
86
+ //
87
+ // NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
88
+ // }
89
+ //
90
+ // deinit {
91
+ // NotificationCenter.default.removeObserver(self)
92
+ // }
93
+ //
94
+ // @objc private func appDidBecomeActive() {
95
+ // if !imgViewLoading.isHidden && imgViewLoading.layer.animation(forKey: "rotationAnimation") == nil {
96
+ // startRotatingImage()
97
+ // }
98
+ // }
99
+ //
100
+ // override func viewWillAppear(_ animated: Bool) {
101
+ // super.viewWillAppear(animated)
102
+ // uiFinishingTouchElements()
103
+ //
104
+ // // Restart rotating animation if missing
105
+ // if !imgViewLoading.isHidden && imgViewLoading.layer.animation(forKey: "rotationAnimation") == nil {
106
+ // startRotatingImage()
107
+ // }
108
+ //
109
+ // if let chargeId = (threeDSecureStatusResponse?["data"] as? [String: Any])?["charge_id"] as? String,
110
+ // !chargeId.isEmpty {
111
+ // // Charge already found - do nothing
112
+ // return
113
+ // }
114
+ //
115
+ // if countdownRemaining > 0 {
116
+ // imgViewLoading.isHidden = false
117
+ // imgViewPaymentDone.isHidden = true
118
+ // // lblCompleteAuthentication.text = "Click the link below to complete 3DS Authentication.!"
119
+ // if !didTapContinue {
120
+ // lblCompleteAuthentication.text = "Tap Continue to proceed with 3D Secure authentication."
121
+ // }
122
+ // btnDone.isHidden = true
123
+ //
124
+ // if apiStatusCheckTimer == nil {
125
+ // apiStatusCheckTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
126
+ // guard let self = self else { return }
127
+ // if self.countdownRemaining > 0 {
128
+ // self.checkThreeDSecureStatus(completion: nil)
129
+ // }
130
+ // }
131
+ // }
132
+ //
133
+ // } else {
134
+ // imgViewLoading.layer.removeAllAnimations()
135
+ // imgViewLoading.isHidden = true
136
+ // imgViewPaymentDone.isHidden = false
137
+ // imgViewPaymentDone.image = UIImage(systemName: "exclamationmark.circle.fill")
138
+ // imgViewPaymentDone.tintColor = .systemRed
139
+ // // lblCompleteAuthentication.text = "Click the link below to complete 3DS Authentication.!"
140
+ // if !didTapContinue {
141
+ // lblCompleteAuthentication.text = "Tap Continue to proceed with 3D Secure authentication."
142
+ // }
143
+ // btnDone.isHidden = false
144
+ // // lblPaymentLink.isHidden = false
145
+ // viewPaymentDetails.isHidden = false
146
+ //
147
+ // apiStatusCheckTimer?.invalidate()
148
+ // apiStatusCheckTimer = nil
149
+ // }
150
+ // }
151
+ //
152
+ // override func viewWillDisappear(_ animated: Bool) {
153
+ // super.viewWillDisappear(animated)
154
+ // apiStatusCheckTimer?.invalidate()
155
+ // apiStatusCheckTimer = nil
156
+ // oneMinuteCountdownTimer?.cancel() // Cancel the dispatch source timer
157
+ // oneMinuteCountdownTimer = nil
158
+ // }
159
+ //
160
+ // private func showHourglassGIF() {
161
+ // guard let bundlePath = Bundle.easyPayBundle.path(forResource: "hourglass", ofType: "gif"),
162
+ // let gifData = try? Data(contentsOf: URL(fileURLWithPath: bundlePath)),
163
+ // let source = CGImageSourceCreateWithData(gifData as CFData, nil) else {
164
+ // print("⚠️ Could not load GIF from bundle")
165
+ // return
166
+ // }
167
+ //
168
+ // var images: [UIImage] = []
169
+ // var duration: Double = 0
170
+ //
171
+ // let frameCount = CGImageSourceGetCount(source)
172
+ // for i in 0..<frameCount {
173
+ // if let cgImage = CGImageSourceCreateImageAtIndex(source, i, nil) {
174
+ // let frameDuration = getFrameDuration(from: source, index: i)
175
+ // duration += frameDuration
176
+ // images.append(UIImage(cgImage: cgImage))
177
+ // }
178
+ // }
179
+ //
180
+ // if !images.isEmpty {
181
+ // let animatedImage = UIImage.animatedImage(with: images, duration: duration)
182
+ // imgViewLoading.image = animatedImage
183
+ // imgViewLoading.contentMode = .scaleToFill
184
+ // }
185
+ // }
186
+ //
187
+ // private func getFrameDuration(from source: CGImageSource, index: Int) -> Double {
188
+ // let defaultFrameDuration = 0.1
189
+ //
190
+ // guard let properties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [CFString: Any],
191
+ // let gifProperties = properties[kCGImagePropertyGIFDictionary] as? [CFString: Any] else {
192
+ // return defaultFrameDuration
193
+ // }
194
+ //
195
+ // if let unclampedDelay = gifProperties[kCGImagePropertyGIFUnclampedDelayTime] as? Double, unclampedDelay > 0 {
196
+ // return unclampedDelay
197
+ // }
198
+ //
199
+ // if let delay = gifProperties[kCGImagePropertyGIFDelayTime] as? Double, delay > 0 {
200
+ // return delay
201
+ // }
202
+ //
203
+ // return defaultFrameDuration
204
+ // }
205
+ //
206
+ // func startRotatingImage() {
207
+ // showHourglassGIF()
208
+ // }
209
+ //
210
+ // @IBAction func actionBtnContinue(_ sender: UIButton) {
211
+ // guard let urlStr = redirectURL, !urlStr.isEmpty else {
212
+ // print("Redirect URL is nil or empty")
213
+ // return
214
+ // }
215
+ //
216
+ // didTapContinue = true
217
+ // lblCompleteAuthentication.text = "Wating for 3DS Authentication..."
218
+ // btnContinue.isHidden = true
219
+ //
220
+ // let vc = easymerchantsdk.instantiateViewController(withIdentifier: "PaymentStatusWebViewVC") as! PaymentStatusWebViewVC
221
+ // vc.urlString = urlStr // Pass the URL to WebView VC
222
+ // self.navigationController?.pushViewController(vc, animated: true)
223
+ // }
224
+ //
225
+ // private func startOneMinuteCountdownAndAPICheck() {
226
+ // startRotatingImage()
227
+ // imgViewPaymentDone.isHidden = true
228
+ // imgViewLoading.isHidden = false
229
+ // // lblCompleteAuthentication.text = "Click the link below to complete 3DS Authentication.!"
230
+ // if !didTapContinue {
231
+ // lblCompleteAuthentication.text = "Tap Continue to proceed with 3D Secure authentication."
232
+ // }
233
+ // btnDone.isHidden = true
234
+ //
235
+ // apiStatusCheckTimer?.invalidate()
236
+ // apiStatusCheckTimer = nil
237
+ //
238
+ // initialAPIGroup.enter()
239
+ // checkThreeDSecureStatus { [weak self] in
240
+ // self?.initialAPIGroup.leave()
241
+ // }
242
+ //
243
+ // countdownRemaining = 180
244
+ //
245
+ // oneMinuteCountdownTimer = DispatchSource.makeTimerSource(queue: .main)
246
+ // oneMinuteCountdownTimer?.schedule(deadline: .now(), repeating: .seconds(1))
247
+ // oneMinuteCountdownTimer?.setEventHandler { [weak self] in
248
+ // guard let self = self else { return }
249
+ //
250
+ // self.countdownRemaining -= 1
251
+ // if self.countdownRemaining <= 0 {
252
+ // self.oneMinuteCountdownTimer?.cancel()
253
+ // self.oneMinuteCountdownTimer = nil
254
+ //
255
+ // DispatchQueue.main.async {
256
+ // self.imgViewLoading.layer.removeAllAnimations()
257
+ // self.imgViewLoading.isHidden = true
258
+ // self.imgViewPaymentDone.isHidden = false
259
+ // // self.lblCompleteAuthentication.text = "Click the link below to complete 3DS Authentication.!"
260
+ // if !self.didTapContinue {
261
+ // self.lblCompleteAuthentication.text = "Tap Continue to proceed with 3D Secure authentication."
262
+ // }
263
+ // self.btnDone.isHidden = false
264
+ // // self.lblPaymentLink.isHidden = false
265
+ // self.viewPaymentDetails.isHidden = false
266
+ //
267
+ // if let json = self.threeDSecureStatusResponse,
268
+ // let data = json["data"] as? [String: Any],
269
+ // let chargeId = data["charge_id"],
270
+ // chargeId is NSNull || (chargeId as? String ?? "").isEmpty {
271
+ // self.imgViewPaymentDone.image = UIImage(systemName: "exclamationmark.circle.fill")
272
+ // self.imgViewPaymentDone.tintColor = .systemRed
273
+ //
274
+ // let refToken = data["ref_token"] as? String ?? "N/A"
275
+ // let chargeId = data["charge_id"] as? String ?? "N/A"
276
+ // let transactionId = data["transaction_id"] as? String ?? "N/A"
277
+ // let subscriptionId = data["subscription_id"] as? String ?? "N/A"
278
+ // let createdAt = data["created_at"] as? String ?? ""
279
+ // let status = data["status"] as? String ?? ""
280
+ //
281
+ // let formattedDate: String
282
+ // if let date = self.convertToDate(dateString: createdAt) {
283
+ // formattedDate = self.formatDate(date: date)
284
+ // } else {
285
+ // formattedDate = "N/A"
286
+ // }
287
+ //
288
+ // self.lblChargeId.text = "\(chargeId)"
289
+ // self.lblTransactionId.text = "\(transactionId)"
290
+ // self.lblSubscriptionId.text = "\(subscriptionId)"
291
+ // self.lblDate.text = "\(formattedDate)"
292
+ // // self.lblTotal.text = "$\(self.amount ?? 0)"
293
+ // let rawAmount = Double(self.request?.amount ?? 0)
294
+ // let amountText = String(format: "$%.2f", rawAmount)
295
+ // self.lblTotal.text = "\(amountText)"
296
+ // self.lblRefferenceId.text = "\(refToken)"
297
+ // self.lblStatus.text = "\(status)"
298
+ // }
299
+ // }
300
+ // }
301
+ // }
302
+ // oneMinuteCountdownTimer?.resume()
303
+ //
304
+ // // API check timer: runs continuously every 5 seconds until success/failure
305
+ // apiStatusCheckTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
306
+ // guard let self = self else { return }
307
+ // self.checkThreeDSecureStatus(completion: nil)
308
+ // }
309
+ // }
310
+ //
311
+ // @IBAction func actionBtnDone(_ sender: UIButton) {
312
+ // var resultBillingInfo: [String: Any]? = nil
313
+ // // var resultAdditionalInfo: [String: Any]? = nil
314
+ //
315
+ // // Extract last 4 digits and format as ****4242
316
+ // if let cardNumber = cardApiParams?["card_number"] as? String, cardNumber.count >= 4 {
317
+ // let last4 = String(cardNumber.suffix(4))
318
+ // let masked = "****\(last4)"
319
+ // chargeData["card_number_last_4"] = masked
320
+ // }
321
+ //
322
+ // // Assign billing info if non-empty
323
+ // if let billing = billingInfo, !billing.isEmpty {
324
+ // resultBillingInfo = billing
325
+ // }
326
+ //
327
+ // // Create response dictionary
328
+ // var response: [String: Any] = [:]
329
+ //
330
+ // // Add 3DS status response data if available, otherwise use charge data
331
+ // if let threeDS = threeDSecureStatusResponse {
332
+ // // Add all 3DS response data directly, preserving the "data" key structure
333
+ // for (key, value) in threeDS {
334
+ // response[key] = value
335
+ // }
336
+ // } else {
337
+ // // Fallback to older charge data if 3DS status response is not available
338
+ // for (key, value) in chargeData {
339
+ // response[key] = value
340
+ // }
341
+ // }
342
+ //
343
+ // // Add billing info if available
344
+ // if let billing = resultBillingInfo {
345
+ // response["billingInfo"] = billing
346
+ // }
347
+ //
348
+ // // Add additional info if available
349
+ // if let additional = additionalInfo {
350
+ // response["additionalInfo"] = additional
351
+ // }
352
+ //
353
+ // // Add card number last 4 if available (from chargeData) - only if not already in response
354
+ // if let last4 = chargeData["card_number_last_4"] as? String, response["card_number_last_4"] == nil {
355
+ // response["card_number_last_4"] = last4
356
+ // }
357
+ //
358
+ // // Create SDK result with combined data
359
+ // let result = SDKResult(type: .success, data: response as NSDictionary)
360
+ //
361
+ // // Notify delegate and dismiss
362
+ // easyPayDelegate?.easyPayController(self.navigationController as! EasyPayViewController, didFinishWith: result)
363
+ //
364
+ // if let easyPayVC = self.navigationController as? EasyPayViewController {
365
+ // easyPayVC.dismiss(animated: true, completion: {
366
+ // if let delegate = easyPayVC.easyPayDelegate {
367
+ // UserStoreSingleton.shared.isLoggedIn = false
368
+ // delegate.easyPayController(easyPayVC, didFinishWith: result)
369
+ // }
370
+ // })
371
+ // }
372
+ // }
373
+ //
374
+ // func uiFinishingTouchElements() {
375
+ // // Set background color for the main view
376
+ // if let containerBGcolor = UserStoreSingleton.shared.container_bg_col,
377
+ // let uiColor = UIColor(hex: containerBGcolor) {
378
+ // self.view.backgroundColor = uiColor
379
+ // self.viewMain.backgroundColor = uiColor
380
+ // self.imgViewLoading.backgroundColor = uiColor
381
+ // // self.viewOverlay.backgroundColor = uiColor.withAlphaComponent(0.3)
382
+ // }
383
+ //
384
+ // if let primaryBtnBackGroundColor = UserStoreSingleton.shared.primary_btn_bg_col,
385
+ // let uiColor = UIColor(hex: primaryBtnBackGroundColor) {
386
+ // btnDone.backgroundColor = uiColor
387
+ // imgViewLoading.image = UIImage(systemName: "arrow.2.circlepath")?.withRenderingMode(.alwaysTemplate)
388
+ // imgViewLoading.tintColor = uiColor
389
+ //
390
+ // btnContinue.backgroundColor = uiColor
391
+ // }
392
+ //
393
+ // if let primaryBtnFontColor = UserStoreSingleton.shared.primary_btn_font_col,
394
+ // let secondaryUIColor = UIColor(hex: primaryBtnFontColor) {
395
+ // btnDone.setTitleColor(secondaryUIColor, for: .normal)
396
+ // btnContinue.setTitleColor(secondaryUIColor, for: .normal)
397
+ // }
398
+ //
399
+ // if let primaryFontColor = UserStoreSingleton.shared.primary_font_col,
400
+ // let uiColor = UIColor(hex: primaryFontColor) {
401
+ // lblDateHeading.textColor = uiColor
402
+ // lblTransactionHeading.textColor = uiColor
403
+ // lblSubscriptionHeading.textColor = uiColor
404
+ // lblChargeIdHeading.textColor = uiColor
405
+ // lblReferenceIdHeading.textColor = uiColor
406
+ // lblPaymentMethodHeading.textColor = uiColor
407
+ // lblTotalHeading.textColor = uiColor
408
+ // lblDate.textColor = uiColor
409
+ // lblTransactionId.textColor = uiColor
410
+ // lblSubscriptionId.textColor = uiColor
411
+ // lblChargeId.textColor = uiColor
412
+ // lblTotal.textColor = uiColor
413
+ // lblRefferenceId.textColor = uiColor
414
+ // lblPaymentMethodValue.textColor = uiColor
415
+ // lblStatusHeading.textColor = uiColor
416
+ // lblStatus.textColor = uiColor
417
+ // }
418
+ //
419
+ // if let borderRadiusString = UserStoreSingleton.shared.border_radious,
420
+ // let borderRadius = Double(borderRadiusString) { // Convert String to Double
421
+ // btnDone.layer.cornerRadius = CGFloat(borderRadius) // Set corner radius
422
+ // } else {
423
+ // btnDone.layer.cornerRadius = 8 // Default value
424
+ // }
425
+ // btnDone.layer.masksToBounds = true // Ensure the corners are clipped properly
426
+ //
427
+ // if let primaryFontColor = UserStoreSingleton.shared.primary_font_col,
428
+ // let uiColor = UIColor(hex: primaryFontColor) {
429
+ // lblCompleteAuthentication.textColor = uiColor
430
+ // lblBillingInfoData.textColor = uiColor
431
+ // }
432
+ //
433
+ // if let fontSizeString = UserStoreSingleton.shared.fontSize,
434
+ // let fontSizeDouble = Double(fontSizeString) { // Convert String to Double
435
+ // let fontSize = CGFloat(fontSizeDouble) // Convert Double to CGFloat
436
+ // // lblCompleteAuthentication.font = UIFont.systemFont(ofSize: fontSize, weight: .semibold)
437
+ // // lblPaymentLink.font = UIFont.systemFont(ofSize: fontSize, weight: .medium)
438
+ // lblBillingInfoData.font = UIFont.systemFont(ofSize: fontSize, weight: .medium)
439
+ // }
440
+ //
441
+ // }
442
+ //
443
+ // private func convertToDate(dateString: String) -> Date? {
444
+ // let formatter = DateFormatter()
445
+ // formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
446
+ // formatter.timeZone = TimeZone(abbreviation: "UTC")
447
+ // return formatter.date(from: dateString)
448
+ // }
449
+ //
450
+ // private func formatDate(date: Date) -> String {
451
+ // let formatter = DateFormatter()
452
+ // formatter.dateFormat = "dd/MM/yyyy, HH:mm:ss"
453
+ // formatter.timeZone = TimeZone.current
454
+ // return formatter.string(from: date)
455
+ // }
456
+ //
457
+ // // MARK: - GET Transaction Status API
458
+ // private func checkThreeDSecureStatus(completion: (() -> Void)?) {
459
+ // guard let secureToken = chargeData["secure_token"] as? String else {
460
+ // print("Secure token not found in chargeData")
461
+ // completion?()
462
+ // return
463
+ // }
464
+ //
465
+ // let urlString = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.threeDSecureStatus(secureToken).path()
466
+ // print("Final 3DS status URL: \(urlString)")
467
+ //
468
+ // guard let url = URL(string: urlString) else {
469
+ // print("Invalid URL")
470
+ // completion?()
471
+ // return
472
+ // }
473
+ //
474
+ // var uRLRequest = URLRequest(url: url)
475
+ // uRLRequest.httpMethod = "GET"
476
+ // uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
477
+ //
478
+ // let token = UserStoreSingleton.shared.clientToken ?? ""
479
+ // uRLRequest.addValue(token, forHTTPHeaderField: "clientToken")
480
+ //
481
+ // let task = URLSession.shared.dataTask(with: uRLRequest) { [weak self] data, response, error in
482
+ // defer { completion?() }
483
+ //
484
+ // guard let self = self else { return }
485
+ //
486
+ // if let error = error {
487
+ // print("3DS status check error:", error.localizedDescription)
488
+ // return
489
+ // }
490
+ //
491
+ // guard let data = data else {
492
+ // print("No data in response")
493
+ // return
494
+ // }
495
+ //
496
+ // do {
497
+ // if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
498
+ // print("3DS status response:", json)
499
+ // self.threeDSecureStatusResponse = json
500
+ //
501
+ // if let dataDict = json["data"] as? [String: Any] {
502
+ // DispatchQueue.main.async {
503
+ // let chargeId = dataDict["charge_id"] as? String ?? "N/A"
504
+ // let transactionId = dataDict["transaction_id"] as? String ?? "N/A"
505
+ // let subscriptionId = dataDict["subscription_id"] as? String ?? "N/A"
506
+ // let createdAt = dataDict["created_at"] as? String ?? ""
507
+ // let referenceId = dataDict["ref_token"] as? String ?? "N/A"
508
+ // let status = dataDict["status"] as? String ?? "N/A"
509
+ //
510
+ // let formattedDate: String
511
+ // if let date = self.convertToDate(dateString: createdAt) {
512
+ // formattedDate = self.formatDate(date: date)
513
+ // } else {
514
+ // formattedDate = "N/A"
515
+ // }
516
+ //
517
+ // self.lblChargeId.text = chargeId
518
+ // self.lblTransactionId.text = transactionId
519
+ // self.lblSubscriptionId.text = subscriptionId
520
+ // self.lblDate.text = formattedDate
521
+ // // self.lblTotal.text = "$\(self.amount ?? 0)"
522
+ // let rawAmount = Double(self.request?.amount ?? 0)
523
+ // let amountText = String(format: "$%.2f", rawAmount)
524
+ // self.lblTotal.text = "\(amountText)"
525
+ //
526
+ // self.lblRefferenceId.text = referenceId
527
+ // self.lblStatus.text = status
528
+ //
529
+ // if status.lowercased() == "failed" {
530
+ // self.imgViewPaymentDone.image = UIImage(named: "payment_error_icon", in: .easyPayBundle, compatibleWith: nil)
531
+ // self.imgViewPaymentDone.tintColor = .systemRed
532
+ // self.apiStatusCheckTimer?.invalidate()
533
+ // self.apiStatusCheckTimer = nil
534
+ // self.oneMinuteCountdownTimer?.cancel()
535
+ // self.oneMinuteCountdownTimer = nil
536
+ //
537
+ // self.imgViewLoading.layer.removeAllAnimations()
538
+ // self.imgViewLoading.isHidden = true
539
+ // self.imgViewPaymentDone.isHidden = false
540
+ // self.lblCompleteAuthentication.text = "Payment Failed"
541
+ // self.btnDone.isHidden = false
542
+ // // self.lblPaymentLink.isHidden = false
543
+ // self.viewPaymentDetails.isHidden = false
544
+ //
545
+ // self.btnContinue.isHidden = true
546
+ // }
547
+ // else if status.lowercased() == "completed" {
548
+ // // Success case
549
+ // self.imgViewPaymentDone.image = UIImage(named: "payment_done_icon", in: .easyPayBundle, compatibleWith: nil)
550
+ // self.apiStatusCheckTimer?.invalidate()
551
+ // self.apiStatusCheckTimer = nil
552
+ // self.oneMinuteCountdownTimer?.cancel()
553
+ // self.oneMinuteCountdownTimer = nil
554
+ //
555
+ // self.imgViewLoading.layer.removeAllAnimations()
556
+ // self.imgViewLoading.isHidden = true
557
+ // self.imgViewPaymentDone.isHidden = false
558
+ // self.lblCompleteAuthentication.text = "Authentication Completed"
559
+ // self.btnDone.isHidden = false
560
+ // // self.lblPaymentLink.isHidden = false
561
+ // self.viewPaymentDetails.isHidden = false
562
+ //
563
+ // self.btnContinue.isHidden = true
564
+ // } else {
565
+ // // Still waiting
566
+ // self.imgViewPaymentDone.image = UIImage(systemName: "exclamationmark.circle.fill")
567
+ // self.imgViewPaymentDone.tintColor = .systemRed
568
+ // }
569
+ // }
570
+ // }
571
+ // }
572
+ // } catch {
573
+ // print("Error parsing JSON:", error.localizedDescription)
574
+ // }
575
+ // }
576
+ // task.resume()
577
+ // }
578
+ //
579
+ //}
580
+ //
581
+
582
+
583
+
584
+
585
+
586
+
587
+
588
+
589
+
590
+
591
+
592
+
593
+
594
+
595
+
596
+
597
+
598
+
599
+
600
+
601
+
602
+
603
+
604
+
605
+
606
+
607
+
608
+
609
+
610
+
611
+
612
+
613
+
614
+
615
+
616
+
617
+
618
+
619
+
620
+
621
+
622
+ import UIKit
623
+
624
+ class ThreeDSecurePaymentDoneVC: BaseVC {
625
+
626
+ @IBOutlet weak var imgViewPaymentDone: UIImageView!
627
+ @IBOutlet weak var imgViewLoading: UIImageView!
628
+ @IBOutlet weak var viewMain: UIView!
629
+ @IBOutlet weak var lblCompleteAuthentication: UILabel!
630
+ @IBOutlet weak var btnDone: UIButton!
631
+ @IBOutlet weak var lblBillingInfoData: UILabel!
632
+
633
+ @IBOutlet weak var viewPaymentDetails: UIStackView!
634
+ @IBOutlet weak var lblDateHeading: UILabel!
635
+ @IBOutlet weak var lblTransactionHeading: UILabel!
636
+ @IBOutlet weak var lblSubscriptionHeading: UILabel!
637
+ @IBOutlet weak var lblChargeIdHeading: UILabel!
638
+ @IBOutlet weak var lblReferenceIdHeading: UILabel!
639
+ @IBOutlet weak var lblPaymentMethodHeading: UILabel!
640
+ @IBOutlet weak var lblTotalHeading: UILabel!
641
+ @IBOutlet weak var lblStatusHeading: UILabel!
642
+
643
+ @IBOutlet weak var lblDate: UILabel!
644
+ @IBOutlet weak var lblTransactionId: UILabel!
645
+ @IBOutlet weak var lblSubscriptionId: UILabel!
646
+ @IBOutlet weak var lblChargeId: UILabel!
647
+ @IBOutlet weak var lblTotal: UILabel!
648
+ @IBOutlet weak var lblRefferenceId: UILabel!
649
+ @IBOutlet weak var lblPaymentMethodValue: UILabel!
650
+ @IBOutlet weak var lblStatus: UILabel!
651
+
652
+ @IBOutlet weak var btnContinue: UIButton!
653
+
654
+ var easyPayDelegate: EasyPayViewControllerDelegate?
655
+ var redirectURL: String?
656
+ var chargeData: [String: Any] = [:]
657
+
658
+ var billingInfo: [String: Any]?
659
+ var additionalInfo: [String: Any]?
660
+
661
+ var threeDSecureStatusResponse: [String: Any]?
662
+
663
+ var apiStatusCheckTimer: Timer?
664
+ var oneMinuteCountdownTimer: DispatchSourceTimer?
665
+ var countdownRemaining: Int = 180
666
+
667
+ var additionalInfoData: [FieldItem]?
668
+ var billingInfoData: [FieldItem]?
669
+ var visibility: FieldsVisibility?
670
+
671
+ let initialAPIGroup = DispatchGroup()
672
+
673
+ var amount: Double?
674
+
675
+ var cardApiParams: [String: Any]?
676
+
677
+ var request: Request!
678
+
679
+ var didTapContinue = false
680
+ var apiAttemptCount: Int = 0
681
+
682
+ override func viewDidLoad() {
683
+ super.viewDidLoad()
684
+ uiFinishingTouchElements()
685
+
686
+ // Initially hide done image and show loading image
687
+ imgViewPaymentDone.isHidden = true
688
+ imgViewLoading.isHidden = false
689
+ lblCompleteAuthentication.text = "Completing Authentication..."
690
+ btnDone.isHidden = true
691
+ btnContinue.isHidden = false
692
+
693
+ // Populate initial label values
694
+ populateInitialLabels()
695
+
696
+ startRotatingImage()
697
+ startOneMinuteCountdownAndAPICheck()
698
+
699
+ NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
700
+ }
701
+
702
+ deinit {
703
+ NotificationCenter.default.removeObserver(self)
704
+ }
705
+
706
+ @objc private func appDidBecomeActive() {
707
+ if !imgViewLoading.isHidden && imgViewLoading.layer.animation(forKey: "rotationAnimation") == nil {
708
+ startRotatingImage()
709
+ }
710
+ }
711
+
712
+ override func viewWillAppear(_ animated: Bool) {
713
+ super.viewWillAppear(animated)
714
+ uiFinishingTouchElements()
715
+
716
+ // Restart rotating animation if missing
717
+ if !imgViewLoading.isHidden && imgViewLoading.layer.animation(forKey: "rotationAnimation") == nil {
718
+ startRotatingImage()
719
+ }
720
+
721
+ if let chargeId = (threeDSecureStatusResponse?["data"] as? [String: Any])?["charge_id"] as? String,
722
+ !chargeId.isEmpty {
723
+ // Charge already found - do nothing
724
+ return
725
+ }
726
+
727
+ if countdownRemaining > 0 {
728
+ imgViewLoading.isHidden = false
729
+ imgViewPaymentDone.isHidden = true
730
+ if !didTapContinue {
731
+ lblCompleteAuthentication.text = "Tap Continue to proceed with 3D Secure authentication."
732
+ }
733
+ btnDone.isHidden = true
734
+ btnContinue.isHidden = false
735
+
736
+ if apiStatusCheckTimer == nil {
737
+ apiStatusCheckTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
738
+ guard let self = self else { return }
739
+ if self.countdownRemaining > 0 {
740
+ self.checkThreeDSecureStatus(completion: nil)
741
+ }
742
+ }
743
+ }
744
+ } else {
745
+ imgViewLoading.layer.removeAllAnimations()
746
+ imgViewLoading.isHidden = true
747
+ imgViewPaymentDone.isHidden = false
748
+ imgViewPaymentDone.image = UIImage(systemName: "exclamationmark.circle.fill")
749
+ imgViewPaymentDone.tintColor = .systemRed
750
+ if !didTapContinue {
751
+ lblCompleteAuthentication.text = "Tap Continue to proceed with 3D Secure authentication."
752
+ }
753
+ btnDone.isHidden = false
754
+ viewPaymentDetails.isHidden = false
755
+ btnContinue.isHidden = true
756
+
757
+ apiStatusCheckTimer?.invalidate()
758
+ apiStatusCheckTimer = nil
759
+ }
760
+ }
761
+
762
+ override func viewWillDisappear(_ animated: Bool) {
763
+ super.viewWillDisappear(animated)
764
+ apiStatusCheckTimer?.invalidate()
765
+ apiStatusCheckTimer = nil
766
+ oneMinuteCountdownTimer?.cancel()
767
+ oneMinuteCountdownTimer = nil
768
+ }
769
+
770
+ private func showHourglassGIF() {
771
+ guard let bundlePath = Bundle.easyPayBundle.path(forResource: "hourglass", ofType: "gif"),
772
+ let gifData = try? Data(contentsOf: URL(fileURLWithPath: bundlePath)),
773
+ let source = CGImageSourceCreateWithData(gifData as CFData, nil) else {
774
+ print("⚠️ Could not load hourglass GIF from bundle, falling back to system image")
775
+ imgViewLoading.image = UIImage(systemName: "hourglass")?.withRenderingMode(.alwaysTemplate)
776
+ imgViewLoading.tintColor = UIColor.systemGray
777
+ imgViewLoading.contentMode = .scaleAspectFit
778
+ return
779
+ }
780
+
781
+ var images: [UIImage] = []
782
+ var duration: Double = 0
783
+
784
+ let frameCount = CGImageSourceGetCount(source)
785
+ for i in 0..<frameCount {
786
+ if let cgImage = CGImageSourceCreateImageAtIndex(source, i, nil) {
787
+ let frameDuration = getFrameDuration(from: source, index: i)
788
+ duration += frameDuration
789
+ images.append(UIImage(cgImage: cgImage))
790
+ }
791
+ }
792
+
793
+ if !images.isEmpty {
794
+ let animatedImage = UIImage.animatedImage(with: images, duration: duration)
795
+ imgViewLoading.image = animatedImage
796
+ imgViewLoading.contentMode = .scaleToFill
797
+ } else {
798
+ print("⚠️ Failed to create animated GIF, falling back to system image")
799
+ imgViewLoading.image = UIImage(systemName: "hourglass")?.withRenderingMode(.alwaysTemplate)
800
+ imgViewLoading.tintColor = UIColor.systemGray
801
+ imgViewLoading.contentMode = .scaleAspectFit
802
+ }
803
+ }
804
+
805
+ private func getFrameDuration(from source: CGImageSource, index: Int) -> Double {
806
+ let defaultFrameDuration = 0.1
807
+
808
+ guard let properties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [CFString: Any],
809
+ let gifProperties = properties[kCGImagePropertyGIFDictionary] as? [CFString: Any] else {
810
+ return defaultFrameDuration
811
+ }
812
+
813
+ if let unclampedDelay = gifProperties[kCGImagePropertyGIFUnclampedDelayTime] as? Double, unclampedDelay > 0 {
814
+ return unclampedDelay
815
+ }
816
+
817
+ if let delay = gifProperties[kCGImagePropertyGIFDelayTime] as? Double, delay > 0 {
818
+ return delay
819
+ }
820
+
821
+ return defaultFrameDuration
822
+ }
823
+
824
+ func startRotatingImage() {
825
+ showHourglassGIF()
826
+ }
827
+
828
+ @IBAction func actionBtnContinue(_ sender: UIButton) {
829
+ guard let urlStr = redirectURL, !urlStr.isEmpty else {
830
+ print("Redirect URL is nil or empty")
831
+ return
832
+ }
833
+
834
+ didTapContinue = true
835
+ lblCompleteAuthentication.text = "Waiting for 3DS Authentication..."
836
+ btnContinue.isHidden = true
837
+
838
+ let vc = easymerchantsdk.instantiateViewController(withIdentifier: "PaymentStatusWebViewVC") as! PaymentStatusWebViewVC
839
+ vc.urlString = urlStr
840
+ self.navigationController?.pushViewController(vc, animated: true)
841
+ }
842
+
843
+ private func populateInitialLabels() {
844
+ // Populate labels with initial data from chargeData or defaults
845
+ let refToken = chargeData["ref_token"] as? String ?? "N/A"
846
+ let chargeId = chargeData["charge_id"] as? String ?? "N/A"
847
+ let transactionId = chargeData["transaction_id"] as? String ?? "N/A"
848
+ let subscriptionId = chargeData["subscription_id"] as? String ?? "N/A"
849
+ let createdAt = chargeData["created_at"] as? String ?? ""
850
+ let status = chargeData["status"] as? String ?? "N/A"
851
+
852
+ let formattedDate: String
853
+ if let date = convertToDate(dateString: createdAt) {
854
+ formattedDate = formatDate(date: date)
855
+ } else {
856
+ formattedDate = "N/A"
857
+ }
858
+
859
+ lblChargeId.text = chargeId
860
+ lblTransactionId.text = transactionId
861
+ lblSubscriptionId.text = subscriptionId
862
+ lblDate.text = formattedDate
863
+ let rawAmount = Double(request?.amount ?? 0)
864
+ let amountText = String(format: "$%.2f", rawAmount)
865
+ lblTotal.text = amountText
866
+ lblRefferenceId.text = refToken
867
+ lblStatus.text = status
868
+ lblPaymentMethodValue.text = "Card" // Set to "Card" by default
869
+ }
870
+
871
+ private func startOneMinuteCountdownAndAPICheck() {
872
+ startRotatingImage()
873
+ imgViewPaymentDone.isHidden = true
874
+ imgViewLoading.isHidden = false
875
+ if !didTapContinue {
876
+ lblCompleteAuthentication.text = "Tap Continue to proceed with 3D Secure authentication."
877
+ }
878
+ btnDone.isHidden = true
879
+ btnContinue.isHidden = false
880
+
881
+ apiStatusCheckTimer?.invalidate()
882
+ apiStatusCheckTimer = nil
883
+ apiAttemptCount = 0
884
+
885
+ initialAPIGroup.enter()
886
+ checkThreeDSecureStatus { [weak self] in
887
+ self?.initialAPIGroup.leave()
888
+ }
889
+
890
+ countdownRemaining = 180
891
+
892
+ oneMinuteCountdownTimer = DispatchSource.makeTimerSource(queue: .main)
893
+ oneMinuteCountdownTimer?.schedule(deadline: .now(), repeating: .seconds(1))
894
+ oneMinuteCountdownTimer?.setEventHandler { [weak self] in
895
+ guard let self = self else { return }
896
+
897
+ self.countdownRemaining -= 1
898
+ if self.countdownRemaining <= 0 {
899
+ self.oneMinuteCountdownTimer?.cancel()
900
+ self.oneMinuteCountdownTimer = nil
901
+
902
+ DispatchQueue.main.async {
903
+ self.imgViewLoading.layer.removeAllAnimations()
904
+ self.imgViewLoading.isHidden = true
905
+ self.imgViewPaymentDone.isHidden = false
906
+ self.imgViewPaymentDone.image = UIImage(systemName: "exclamationmark.circle.fill")
907
+ self.imgViewPaymentDone.tintColor = .systemRed
908
+ if !self.didTapContinue {
909
+ self.lblCompleteAuthentication.text = "Tap Continue to proceed with 3D Secure authentication."
910
+ }
911
+ self.btnDone.isHidden = false
912
+ self.viewPaymentDetails.isHidden = false
913
+ self.btnContinue.isHidden = true
914
+
915
+ if let json = self.threeDSecureStatusResponse,
916
+ let data = json["data"] as? [String: Any] {
917
+ let refToken = data["ref_token"] as? String ?? "N/A"
918
+ let chargeId = data["charge_id"] as? String ?? "N/A"
919
+ let transactionId = data["transaction_id"] as? String ?? "N/A"
920
+ let subscriptionId = data["subscription_id"] as? String ?? "N/A"
921
+ let createdAt = data["created_at"] as? String ?? ""
922
+
923
+ let formattedDate: String
924
+ if let date = self.convertToDate(dateString: createdAt) {
925
+ formattedDate = self.formatDate(date: date)
926
+ } else {
927
+ formattedDate = "N/A"
928
+ }
929
+
930
+ self.lblChargeId.text = chargeId
931
+ self.lblTransactionId.text = transactionId
932
+ self.lblSubscriptionId.text = subscriptionId
933
+ self.lblDate.text = formattedDate
934
+ let rawAmount = Double(self.request?.amount ?? 0)
935
+ let amountText = String(format: "$%.2f", rawAmount)
936
+ self.lblTotal.text = amountText
937
+ self.lblRefferenceId.text = refToken
938
+ self.lblPaymentMethodValue.text = "Card"
939
+ }
940
+ // Force cancelled status after 180 seconds if user didn't tap Continue
941
+ self.lblStatus.text = "cancelled"
942
+ }
943
+ }
944
+ }
945
+ oneMinuteCountdownTimer?.resume()
946
+
947
+ apiStatusCheckTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
948
+ guard let self = self else { return }
949
+ self.checkThreeDSecureStatus(completion: nil)
950
+ }
951
+ }
952
+
953
+ @IBAction func actionBtnDone(_ sender: UIButton) {
954
+ var resultBillingInfo: [String: Any]? = nil
955
+
956
+ if let cardNumber = cardApiParams?["card_number"] as? String, cardNumber.count >= 4 {
957
+ let last4 = String(cardNumber.suffix(4))
958
+ let masked = "****\(last4)"
959
+ chargeData["card_number_last_4"] = masked
960
+ }
961
+
962
+ if let billing = billingInfo, !billing.isEmpty {
963
+ resultBillingInfo = billing
964
+ }
965
+
966
+ var response: [String: Any] = [:]
967
+
968
+ if let threeDS = threeDSecureStatusResponse {
969
+ for (key, value) in threeDS {
970
+ response[key] = value
971
+ }
972
+ } else {
973
+ for (key, value) in chargeData {
974
+ response[key] = value
975
+ }
976
+ }
977
+
978
+ if let billing = resultBillingInfo {
979
+ response["billingInfo"] = billing
980
+ }
981
+
982
+ if let additional = additionalInfo {
983
+ response["additionalInfo"] = additional
984
+ }
985
+
986
+ if let last4 = chargeData["card_number_last_4"] as? String, response["card_number_last_4"] == nil {
987
+ response["card_number_last_4"] = last4
988
+ }
989
+
990
+ let result = SDKResult(type: .success, data: response as NSDictionary)
991
+
992
+ easyPayDelegate?.easyPayController(self.navigationController as! EasyPayViewController, didFinishWith: result)
993
+
994
+ if let easyPayVC = self.navigationController as? EasyPayViewController {
995
+ easyPayVC.dismiss(animated: true, completion: {
996
+ if let delegate = easyPayVC.easyPayDelegate {
997
+ UserStoreSingleton.shared.isLoggedIn = false
998
+ delegate.easyPayController(easyPayVC, didFinishWith: result)
999
+ }
1000
+ })
1001
+ }
1002
+ }
1003
+
1004
+ func uiFinishingTouchElements() {
1005
+ if let containerBGcolor = UserStoreSingleton.shared.container_bg_col,
1006
+ let uiColor = UIColor(hex: containerBGcolor) {
1007
+ self.view.backgroundColor = uiColor
1008
+ self.viewMain.backgroundColor = uiColor
1009
+ self.imgViewLoading.backgroundColor = uiColor
1010
+ }
1011
+
1012
+ if let primaryBtnBackGroundColor = UserStoreSingleton.shared.primary_btn_bg_col,
1013
+ let uiColor = UIColor(hex: primaryBtnBackGroundColor) {
1014
+ btnDone.backgroundColor = uiColor
1015
+ // Removed imgViewLoading.image assignment to avoid overriding GIF
1016
+ imgViewLoading.tintColor = uiColor
1017
+ btnContinue.backgroundColor = uiColor
1018
+ }
1019
+
1020
+ if let primaryBtnFontColor = UserStoreSingleton.shared.primary_btn_font_col,
1021
+ let secondaryUIColor = UIColor(hex: primaryBtnFontColor) {
1022
+ btnDone.setTitleColor(secondaryUIColor, for: .normal)
1023
+ btnContinue.setTitleColor(secondaryUIColor, for: .normal)
1024
+ }
1025
+
1026
+ if let primaryFontColor = UserStoreSingleton.shared.primary_font_col,
1027
+ let uiColor = UIColor(hex: primaryFontColor) {
1028
+ lblDateHeading.textColor = uiColor
1029
+ lblTransactionHeading.textColor = uiColor
1030
+ lblSubscriptionHeading.textColor = uiColor
1031
+ lblChargeIdHeading.textColor = uiColor
1032
+ lblReferenceIdHeading.textColor = uiColor
1033
+ lblPaymentMethodHeading.textColor = uiColor
1034
+ lblTotalHeading.textColor = uiColor
1035
+ lblDate.textColor = uiColor
1036
+ lblTransactionId.textColor = uiColor
1037
+ lblSubscriptionId.textColor = uiColor
1038
+ lblChargeId.textColor = uiColor
1039
+ lblTotal.textColor = uiColor
1040
+ lblRefferenceId.textColor = uiColor
1041
+ lblPaymentMethodValue.textColor = uiColor
1042
+ lblStatusHeading.textColor = uiColor
1043
+ lblStatus.textColor = uiColor
1044
+ }
1045
+
1046
+ if let borderRadiusString = UserStoreSingleton.shared.border_radious,
1047
+ let borderRadius = Double(borderRadiusString) {
1048
+ btnDone.layer.cornerRadius = CGFloat(borderRadius)
1049
+ } else {
1050
+ btnDone.layer.cornerRadius = 8
1051
+ }
1052
+ btnDone.layer.masksToBounds = true
1053
+
1054
+ if let primaryFontColor = UserStoreSingleton.shared.primary_font_col,
1055
+ let uiColor = UIColor(hex: primaryFontColor) {
1056
+ lblCompleteAuthentication.textColor = uiColor
1057
+ lblBillingInfoData.textColor = uiColor
1058
+ }
1059
+
1060
+ if let fontSizeString = UserStoreSingleton.shared.fontSize,
1061
+ let fontSizeDouble = Double(fontSizeString) {
1062
+ let fontSize = CGFloat(fontSizeDouble)
1063
+ lblBillingInfoData.font = UIFont.systemFont(ofSize: fontSize, weight: .medium)
1064
+ }
1065
+ }
1066
+
1067
+ private func convertToDate(dateString: String) -> Date? {
1068
+ let formatter = DateFormatter()
1069
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
1070
+ formatter.timeZone = TimeZone(abbreviation: "UTC")
1071
+ return formatter.date(from: dateString)
1072
+ }
1073
+
1074
+ private func formatDate(date: Date) -> String {
1075
+ let formatter = DateFormatter()
1076
+ formatter.dateFormat = "dd/MM/yyyy, HH:mm:ss"
1077
+ formatter.timeZone = TimeZone.current
1078
+ return formatter.string(from: date)
1079
+ }
1080
+
1081
+ private func checkThreeDSecureStatus(completion: (() -> Void)?) {
1082
+ guard let secureToken = chargeData["secure_token"] as? String else {
1083
+ print("Secure token not found in chargeData")
1084
+ completion?()
1085
+ return
1086
+ }
1087
+
1088
+ apiAttemptCount += 1
1089
+
1090
+ let urlString = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.threeDSecureStatus(secureToken).path()
1091
+ print("Final 3DS status URL: \(urlString)")
1092
+
1093
+ guard let url = URL(string: urlString) else {
1094
+ print("Invalid URL")
1095
+ completion?()
1096
+ return
1097
+ }
1098
+
1099
+ var uRLRequest = URLRequest(url: url)
1100
+ uRLRequest.httpMethod = "GET"
1101
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
1102
+
1103
+ let token = UserStoreSingleton.shared.clientToken ?? ""
1104
+ uRLRequest.addValue(token, forHTTPHeaderField: "clientToken")
1105
+
1106
+ let task = URLSession.shared.dataTask(with: uRLRequest) { [weak self] data, response, error in
1107
+ defer { completion?() }
1108
+
1109
+ guard let self = self else { return }
1110
+
1111
+ if let error = error {
1112
+ print("3DS status check error:", error.localizedDescription)
1113
+ return
1114
+ }
1115
+
1116
+ guard let data = data else {
1117
+ print("No data in response")
1118
+ return
1119
+ }
1120
+
1121
+ do {
1122
+ if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
1123
+ print("3DS status response:", json)
1124
+ self.threeDSecureStatusResponse = json
1125
+
1126
+ if let dataDict = json["data"] as? [String: Any] {
1127
+ DispatchQueue.main.async {
1128
+ let chargeId = dataDict["charge_id"] as? String ?? "N/A"
1129
+ let transactionId = dataDict["transaction_id"] as? String ?? "N/A"
1130
+ let subscriptionId = dataDict["subscription_id"] as? String ?? "N/A"
1131
+ let createdAt = dataDict["created_at"] as? String ?? ""
1132
+ let referenceId = dataDict["ref_token"] as? String ?? "N/A"
1133
+ let status = dataDict["status"] as? String ?? "N/A"
1134
+
1135
+ let formattedDate: String
1136
+ if let date = self.convertToDate(dateString: createdAt) {
1137
+ formattedDate = self.formatDate(date: date)
1138
+ } else {
1139
+ formattedDate = "N/A"
1140
+ }
1141
+
1142
+ self.lblChargeId.text = chargeId
1143
+ self.lblTransactionId.text = transactionId
1144
+ self.lblSubscriptionId.text = subscriptionId
1145
+ self.lblDate.text = formattedDate
1146
+ let rawAmount = Double(self.request?.amount ?? 0)
1147
+ let amountText = String(format: "$%.2f", rawAmount)
1148
+ self.lblTotal.text = amountText
1149
+ self.lblRefferenceId.text = referenceId
1150
+ self.lblStatus.text = status
1151
+ self.lblPaymentMethodValue.text = "Card" // Ensure "Card" in all cases
1152
+
1153
+ if status.lowercased() == "completed" {
1154
+ // Success case
1155
+ self.imgViewPaymentDone.image = UIImage(named: "payment_done_icon", in: .easyPayBundle, compatibleWith: nil)
1156
+ self.apiStatusCheckTimer?.invalidate()
1157
+ self.apiStatusCheckTimer = nil
1158
+ self.oneMinuteCountdownTimer?.cancel()
1159
+ self.oneMinuteCountdownTimer = nil
1160
+
1161
+ self.imgViewLoading.layer.removeAllAnimations()
1162
+ self.imgViewLoading.isHidden = true
1163
+ self.imgViewPaymentDone.isHidden = false
1164
+ self.lblCompleteAuthentication.text = "Authentication Completed"
1165
+ self.btnDone.isHidden = false
1166
+ self.viewPaymentDetails.isHidden = false
1167
+ self.btnContinue.isHidden = true
1168
+ } else if status.lowercased() == "failed" {
1169
+ // Failure case
1170
+ self.imgViewPaymentDone.image = UIImage(named: "payment_error_icon", in: .easyPayBundle, compatibleWith: nil)
1171
+ self.imgViewPaymentDone.tintColor = .systemRed
1172
+ self.apiStatusCheckTimer?.invalidate()
1173
+ self.apiStatusCheckTimer = nil
1174
+ self.oneMinuteCountdownTimer?.cancel()
1175
+ self.oneMinuteCountdownTimer = nil
1176
+
1177
+ self.imgViewLoading.layer.removeAllAnimations()
1178
+ self.imgViewLoading.isHidden = true
1179
+ self.imgViewPaymentDone.isHidden = false
1180
+ self.lblCompleteAuthentication.text = "Transaction Failed.!"
1181
+ self.btnDone.isHidden = false
1182
+ self.viewPaymentDetails.isHidden = false
1183
+ self.btnContinue.isHidden = true
1184
+ } else {
1185
+ // Non-terminal statuses
1186
+ if self.didTapContinue {
1187
+ // User returned from WebView: after max two attempts, finalize with whatever status we have
1188
+ if self.apiAttemptCount >= 2 {
1189
+ self.apiStatusCheckTimer?.invalidate()
1190
+ self.apiStatusCheckTimer = nil
1191
+ self.oneMinuteCountdownTimer?.cancel()
1192
+ self.oneMinuteCountdownTimer = nil
1193
+
1194
+ self.imgViewLoading.layer.removeAllAnimations()
1195
+ self.imgViewLoading.isHidden = true
1196
+ self.imgViewPaymentDone.isHidden = false
1197
+ self.imgViewPaymentDone.image = UIImage(systemName: "exclamationmark.circle.fill")
1198
+ self.lblCompleteAuthentication.text = "Transaction cancelled.!"
1199
+ self.imgViewPaymentDone.tintColor = .systemRed
1200
+ self.btnDone.isHidden = false
1201
+ self.viewPaymentDetails.isHidden = false
1202
+ self.btnContinue.isHidden = true
1203
+ // Keep lblStatus as received
1204
+ }
1205
+ } else {
1206
+ // Initial waiting period: do not auto-cancel or finalize before 180s
1207
+ // Keep loading and allow polling to continue
1208
+ }
1209
+ }
1210
+ }
1211
+ }
1212
+ }
1213
+ } catch {
1214
+ print("Error parsing JSON:", error.localizedDescription)
1215
+ }
1216
+ }
1217
+ task.resume()
1218
+ }
1219
+ }
1220
+
1221
+
1222
+
1223
+
1224
+
1225
+
1226
+
1227
+
1228
+
1229
+
1230
+
1231
+
1232
+
1233
+
1234
+
1235
+
1236
+
1237
+
1238
+
1239
+
1240
+
1241
+
1242
+
1243
+
1244
+
1245
+
1246
+
1247
+
1248
+
1249
+
1250
+
1251
+
1252
+
1253
+
1254
+