@jimrising/easymerchantsdk-react-native 2.5.1 → 2.5.2

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 (29) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/ios/Pods/Storyboard/EasyPaySdk.storyboard +9089 -0
  3. package/ios/Pods/UserDefaults/UserStoreSingleton.swift +424 -0
  4. package/ios/Pods/ViewControllers/AdditionalInfoVC.swift +2894 -0
  5. package/ios/Pods/ViewControllers/BaseVC.swift +142 -0
  6. package/ios/Pods/ViewControllers/BillingInfoVC/BillingInfoVC.swift +3686 -0
  7. package/ios/Pods/ViewControllers/BillingInfoVC/Cells/CityListTVC.swift +46 -0
  8. package/ios/Pods/ViewControllers/BillingInfoVC/Cells/CountryListTVC.swift +47 -0
  9. package/ios/Pods/ViewControllers/BillingInfoVC/Cells/StateListTVC.swift +46 -0
  10. package/ios/Pods/ViewControllers/Clean Runner_2025-07-23T14-58-05.txt +13 -0
  11. package/ios/Pods/ViewControllers/CountryListVC.swift +435 -0
  12. package/ios/Pods/ViewControllers/EmailVerificationVC.swift +286 -0
  13. package/ios/Pods/ViewControllers/GrailPayVC.swift +483 -0
  14. package/ios/Pods/ViewControllers/OTPVerificationVC.swift +2193 -0
  15. package/ios/Pods/ViewControllers/PaymentDoneVC.swift +284 -0
  16. package/ios/Pods/ViewControllers/PaymentErrorVC.swift +85 -0
  17. package/ios/Pods/ViewControllers/PaymentInformation/AccountTypeTVC.swift +41 -0
  18. package/ios/Pods/ViewControllers/PaymentInformation/PaymentInfoVC.swift +12875 -0
  19. package/ios/Pods/ViewControllers/PaymentInformation/PaymentInformationCVC.swift +35 -0
  20. package/ios/Pods/ViewControllers/PaymentInformation/RecurringTVC.swift +40 -0
  21. package/ios/Pods/ViewControllers/PaymentInformation/SavedAccountsTVC/SavedAccountTVC.swift +80 -0
  22. package/ios/Pods/ViewControllers/PaymentInformation/SavedAccountsTVC/SavedAccountTVC.xib +163 -0
  23. package/ios/Pods/ViewControllers/PaymentInformation/SavedCardsTVC/SavedCardsTVC.swift +81 -0
  24. package/ios/Pods/ViewControllers/PaymentInformation/SavedCardsTVC/SavedCardsTVC.xib +188 -0
  25. package/ios/Pods/ViewControllers/PaymentStatusWebViewVC.swift +158 -0
  26. package/ios/Pods/ViewControllers/TermAndConditionsVC.swift +63 -0
  27. package/ios/Pods/ViewControllers/ThreeDSecurePaymentDoneVC.swift +1216 -0
  28. package/ios/easymerchantsdk.podspec +1 -1
  29. package/package.json +1 -1
@@ -0,0 +1,1216 @@
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
+ // return
165
+ // }
166
+ //
167
+ // var images: [UIImage] = []
168
+ // var duration: Double = 0
169
+ //
170
+ // let frameCount = CGImageSourceGetCount(source)
171
+ // for i in 0..<frameCount {
172
+ // if let cgImage = CGImageSourceCreateImageAtIndex(source, i, nil) {
173
+ // let frameDuration = getFrameDuration(from: source, index: i)
174
+ // duration += frameDuration
175
+ // images.append(UIImage(cgImage: cgImage))
176
+ // }
177
+ // }
178
+ //
179
+ // if !images.isEmpty {
180
+ // let animatedImage = UIImage.animatedImage(with: images, duration: duration)
181
+ // imgViewLoading.image = animatedImage
182
+ // imgViewLoading.contentMode = .scaleToFill
183
+ // }
184
+ // }
185
+ //
186
+ // private func getFrameDuration(from source: CGImageSource, index: Int) -> Double {
187
+ // let defaultFrameDuration = 0.1
188
+ //
189
+ // guard let properties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [CFString: Any],
190
+ // let gifProperties = properties[kCGImagePropertyGIFDictionary] as? [CFString: Any] else {
191
+ // return defaultFrameDuration
192
+ // }
193
+ //
194
+ // if let unclampedDelay = gifProperties[kCGImagePropertyGIFUnclampedDelayTime] as? Double, unclampedDelay > 0 {
195
+ // return unclampedDelay
196
+ // }
197
+ //
198
+ // if let delay = gifProperties[kCGImagePropertyGIFDelayTime] as? Double, delay > 0 {
199
+ // return delay
200
+ // }
201
+ //
202
+ // return defaultFrameDuration
203
+ // }
204
+ //
205
+ // func startRotatingImage() {
206
+ // showHourglassGIF()
207
+ // }
208
+ //
209
+ // @IBAction func actionBtnContinue(_ sender: UIButton) {
210
+ // guard let urlStr = redirectURL, !urlStr.isEmpty else {
211
+ // return
212
+ // }
213
+ //
214
+ // didTapContinue = true
215
+ // lblCompleteAuthentication.text = "Wating for 3DS Authentication..."
216
+ // btnContinue.isHidden = true
217
+ //
218
+ // let vc = EasyPaySdk.instantiateViewController(withIdentifier: "PaymentStatusWebViewVC") as! PaymentStatusWebViewVC
219
+ // vc.urlString = urlStr // Pass the URL to WebView VC
220
+ // self.navigationController?.pushViewController(vc, animated: true)
221
+ // }
222
+ //
223
+ // private func startOneMinuteCountdownAndAPICheck() {
224
+ // startRotatingImage()
225
+ // imgViewPaymentDone.isHidden = true
226
+ // imgViewLoading.isHidden = false
227
+ // // lblCompleteAuthentication.text = "Click the link below to complete 3DS Authentication.!"
228
+ // if !didTapContinue {
229
+ // lblCompleteAuthentication.text = "Tap Continue to proceed with 3D Secure authentication."
230
+ // }
231
+ // btnDone.isHidden = true
232
+ //
233
+ // apiStatusCheckTimer?.invalidate()
234
+ // apiStatusCheckTimer = nil
235
+ //
236
+ // initialAPIGroup.enter()
237
+ // checkThreeDSecureStatus { [weak self] in
238
+ // self?.initialAPIGroup.leave()
239
+ // }
240
+ //
241
+ // countdownRemaining = 180
242
+ //
243
+ // oneMinuteCountdownTimer = DispatchSource.makeTimerSource(queue: .main)
244
+ // oneMinuteCountdownTimer?.schedule(deadline: .now(), repeating: .seconds(1))
245
+ // oneMinuteCountdownTimer?.setEventHandler { [weak self] in
246
+ // guard let self = self else { return }
247
+ //
248
+ // self.countdownRemaining -= 1
249
+ // if self.countdownRemaining <= 0 {
250
+ // self.oneMinuteCountdownTimer?.cancel()
251
+ // self.oneMinuteCountdownTimer = nil
252
+ //
253
+ // DispatchQueue.main.async {
254
+ // self.imgViewLoading.layer.removeAllAnimations()
255
+ // self.imgViewLoading.isHidden = true
256
+ // self.imgViewPaymentDone.isHidden = false
257
+ // // self.lblCompleteAuthentication.text = "Click the link below to complete 3DS Authentication.!"
258
+ // if !self.didTapContinue {
259
+ // self.lblCompleteAuthentication.text = "Tap Continue to proceed with 3D Secure authentication."
260
+ // }
261
+ // self.btnDone.isHidden = false
262
+ // // self.lblPaymentLink.isHidden = false
263
+ // self.viewPaymentDetails.isHidden = false
264
+ //
265
+ // if let json = self.threeDSecureStatusResponse,
266
+ // let data = json["data"] as? [String: Any],
267
+ // let chargeId = data["charge_id"],
268
+ // chargeId is NSNull || (chargeId as? String ?? "").isEmpty {
269
+ // self.imgViewPaymentDone.image = UIImage(systemName: "exclamationmark.circle.fill")
270
+ // self.imgViewPaymentDone.tintColor = .systemRed
271
+ //
272
+ // let refToken = data["ref_token"] as? String ?? "N/A"
273
+ // let chargeId = data["charge_id"] as? String ?? "N/A"
274
+ // let transactionId = data["transaction_id"] as? String ?? "N/A"
275
+ // let subscriptionId = data["subscription_id"] as? String ?? "N/A"
276
+ // let createdAt = data["created_at"] as? String ?? ""
277
+ // let status = data["status"] as? String ?? ""
278
+ //
279
+ // let formattedDate: String
280
+ // if let date = self.convertToDate(dateString: createdAt) {
281
+ // formattedDate = self.formatDate(date: date)
282
+ // } else {
283
+ // formattedDate = "N/A"
284
+ // }
285
+ //
286
+ // self.lblChargeId.text = "\(chargeId)"
287
+ // self.lblTransactionId.text = "\(transactionId)"
288
+ // self.lblSubscriptionId.text = "\(subscriptionId)"
289
+ // self.lblDate.text = "\(formattedDate)"
290
+ // // self.lblTotal.text = "$\(self.amount ?? 0)"
291
+ // let rawAmount = Double(self.request?.amount ?? 0)
292
+ // let amountText = String(format: "$%.2f", rawAmount)
293
+ // self.lblTotal.text = "\(amountText)"
294
+ // self.lblRefferenceId.text = "\(refToken)"
295
+ // self.lblStatus.text = "\(status)"
296
+ // }
297
+ // }
298
+ // }
299
+ // }
300
+ // oneMinuteCountdownTimer?.resume()
301
+ //
302
+ // // API check timer: runs continuously every 5 seconds until success/failure
303
+ // apiStatusCheckTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
304
+ // guard let self = self else { return }
305
+ // self.checkThreeDSecureStatus(completion: nil)
306
+ // }
307
+ // }
308
+ //
309
+ // @IBAction func actionBtnDone(_ sender: UIButton) {
310
+ // var resultBillingInfo: [String: Any]? = nil
311
+ // // var resultAdditionalInfo: [String: Any]? = nil
312
+ //
313
+ // // Extract last 4 digits and format as ****4242
314
+ // if let cardNumber = cardApiParams?["card_number"] as? String, cardNumber.count >= 4 {
315
+ // let last4 = String(cardNumber.suffix(4))
316
+ // let masked = "****\(last4)"
317
+ // chargeData["card_number_last_4"] = masked
318
+ // }
319
+ //
320
+ // // Assign billing info if non-empty
321
+ // if let billing = billingInfo, !billing.isEmpty {
322
+ // resultBillingInfo = billing
323
+ // }
324
+ //
325
+ // // Create response dictionary
326
+ // var response: [String: Any] = [:]
327
+ //
328
+ // // Add 3DS status response data if available, otherwise use charge data
329
+ // if let threeDS = threeDSecureStatusResponse {
330
+ // // Add all 3DS response data directly, preserving the "data" key structure
331
+ // for (key, value) in threeDS {
332
+ // response[key] = value
333
+ // }
334
+ // } else {
335
+ // // Fallback to older charge data if 3DS status response is not available
336
+ // for (key, value) in chargeData {
337
+ // response[key] = value
338
+ // }
339
+ // }
340
+ //
341
+ // // Add billing info if available
342
+ // if let billing = resultBillingInfo {
343
+ // response["billingInfo"] = billing
344
+ // }
345
+ //
346
+ // // Add additional info if available
347
+ // if let additional = additionalInfo {
348
+ // response["additionalInfo"] = additional
349
+ // }
350
+ //
351
+ // // Add card number last 4 if available (from chargeData) - only if not already in response
352
+ // if let last4 = chargeData["card_number_last_4"] as? String, response["card_number_last_4"] == nil {
353
+ // response["card_number_last_4"] = last4
354
+ // }
355
+ //
356
+ // // Create SDK result with combined data
357
+ // let result = SDKResult(type: .success, data: response as NSDictionary)
358
+ //
359
+ // // Notify delegate and dismiss
360
+ // easyPayDelegate?.easyPayController(self.navigationController as! EasyPayViewController, didFinishWith: result)
361
+ //
362
+ // if let easyPayVC = self.navigationController as? EasyPayViewController {
363
+ // easyPayVC.dismiss(animated: true, completion: {
364
+ // if let delegate = easyPayVC.easyPayDelegate {
365
+ // UserStoreSingleton.shared.isLoggedIn = false
366
+ // delegate.easyPayController(easyPayVC, didFinishWith: result)
367
+ // }
368
+ // })
369
+ // }
370
+ // }
371
+ //
372
+ // func uiFinishingTouchElements() {
373
+ // // Set background color for the main view
374
+ // if let containerBGcolor = UserStoreSingleton.shared.container_bg_col,
375
+ // let uiColor = UIColor(hex: containerBGcolor) {
376
+ // self.view.backgroundColor = uiColor
377
+ // self.viewMain.backgroundColor = uiColor
378
+ // self.imgViewLoading.backgroundColor = uiColor
379
+ // // self.viewOverlay.backgroundColor = uiColor.withAlphaComponent(0.3)
380
+ // }
381
+ //
382
+ // if let primaryBtnBackGroundColor = UserStoreSingleton.shared.primary_btn_bg_col,
383
+ // let uiColor = UIColor(hex: primaryBtnBackGroundColor) {
384
+ // btnDone.backgroundColor = uiColor
385
+ // imgViewLoading.image = UIImage(systemName: "arrow.2.circlepath")?.withRenderingMode(.alwaysTemplate)
386
+ // imgViewLoading.tintColor = uiColor
387
+ //
388
+ // btnContinue.backgroundColor = uiColor
389
+ // }
390
+ //
391
+ // if let primaryBtnFontColor = UserStoreSingleton.shared.primary_btn_font_col,
392
+ // let secondaryUIColor = UIColor(hex: primaryBtnFontColor) {
393
+ // btnDone.setTitleColor(secondaryUIColor, for: .normal)
394
+ // btnContinue.setTitleColor(secondaryUIColor, for: .normal)
395
+ // }
396
+ //
397
+ // if let primaryFontColor = UserStoreSingleton.shared.primary_font_col,
398
+ // let uiColor = UIColor(hex: primaryFontColor) {
399
+ // lblDateHeading.textColor = uiColor
400
+ // lblTransactionHeading.textColor = uiColor
401
+ // lblSubscriptionHeading.textColor = uiColor
402
+ // lblChargeIdHeading.textColor = uiColor
403
+ // lblReferenceIdHeading.textColor = uiColor
404
+ // lblPaymentMethodHeading.textColor = uiColor
405
+ // lblTotalHeading.textColor = uiColor
406
+ // lblDate.textColor = uiColor
407
+ // lblTransactionId.textColor = uiColor
408
+ // lblSubscriptionId.textColor = uiColor
409
+ // lblChargeId.textColor = uiColor
410
+ // lblTotal.textColor = uiColor
411
+ // lblRefferenceId.textColor = uiColor
412
+ // lblPaymentMethodValue.textColor = uiColor
413
+ // lblStatusHeading.textColor = uiColor
414
+ // lblStatus.textColor = uiColor
415
+ // }
416
+ //
417
+ // if let borderRadiusString = UserStoreSingleton.shared.border_radious,
418
+ // let borderRadius = Double(borderRadiusString) { // Convert String to Double
419
+ // btnDone.layer.cornerRadius = CGFloat(borderRadius) // Set corner radius
420
+ // } else {
421
+ // btnDone.layer.cornerRadius = 8 // Default value
422
+ // }
423
+ // btnDone.layer.masksToBounds = true // Ensure the corners are clipped properly
424
+ //
425
+ // if let primaryFontColor = UserStoreSingleton.shared.primary_font_col,
426
+ // let uiColor = UIColor(hex: primaryFontColor) {
427
+ // lblCompleteAuthentication.textColor = uiColor
428
+ // lblBillingInfoData.textColor = uiColor
429
+ // }
430
+ //
431
+ // if let fontSizeString = UserStoreSingleton.shared.fontSize,
432
+ // let fontSizeDouble = Double(fontSizeString) { // Convert String to Double
433
+ // let fontSize = CGFloat(fontSizeDouble) // Convert Double to CGFloat
434
+ // // lblCompleteAuthentication.font = UIFont.systemFont(ofSize: fontSize, weight: .semibold)
435
+ // // lblPaymentLink.font = UIFont.systemFont(ofSize: fontSize, weight: .medium)
436
+ // lblBillingInfoData.font = UIFont.systemFont(ofSize: fontSize, weight: .medium)
437
+ // }
438
+ //
439
+ // }
440
+ //
441
+ // private func convertToDate(dateString: String) -> Date? {
442
+ // let formatter = DateFormatter()
443
+ // formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
444
+ // formatter.timeZone = TimeZone(abbreviation: "UTC")
445
+ // return formatter.date(from: dateString)
446
+ // }
447
+ //
448
+ // private func formatDate(date: Date) -> String {
449
+ // let formatter = DateFormatter()
450
+ // formatter.dateFormat = "dd/MM/yyyy, HH:mm:ss"
451
+ // formatter.timeZone = TimeZone.current
452
+ // return formatter.string(from: date)
453
+ // }
454
+ //
455
+ // // MARK: - GET Transaction Status API
456
+ // private func checkThreeDSecureStatus(completion: (() -> Void)?) {
457
+ // guard let secureToken = chargeData["secure_token"] as? String else {
458
+ // completion?()
459
+ // return
460
+ // }
461
+ //
462
+ // let urlString = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.threeDSecureStatus(secureToken).path()
463
+ //
464
+ // guard let url = URL(string: urlString) else {
465
+ // completion?()
466
+ // return
467
+ // }
468
+ //
469
+ // var uRLRequest = URLRequest(url: url)
470
+ // uRLRequest.httpMethod = "GET"
471
+ // uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
472
+ //
473
+ // let token = UserStoreSingleton.shared.clientToken ?? ""
474
+ // uRLRequest.addValue(token, forHTTPHeaderField: "clientToken")
475
+ //
476
+ // let task = URLSession.shared.dataTask(with: uRLRequest) { [weak self] data, response, error in
477
+ // defer { completion?() }
478
+ //
479
+ // guard let self = self else { return }
480
+ //
481
+ // if let error = error {
482
+ // return
483
+ // }
484
+ //
485
+ // guard let data = data else {
486
+ // return
487
+ // }
488
+ //
489
+ // do {
490
+ // if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
491
+ // self.threeDSecureStatusResponse = json
492
+ //
493
+ // if let dataDict = json["data"] as? [String: Any] {
494
+ // DispatchQueue.main.async {
495
+ // let chargeId = dataDict["charge_id"] as? String ?? "N/A"
496
+ // let transactionId = dataDict["transaction_id"] as? String ?? "N/A"
497
+ // let subscriptionId = dataDict["subscription_id"] as? String ?? "N/A"
498
+ // let createdAt = dataDict["created_at"] as? String ?? ""
499
+ // let referenceId = dataDict["ref_token"] as? String ?? "N/A"
500
+ // let status = dataDict["status"] as? String ?? "N/A"
501
+ //
502
+ // let formattedDate: String
503
+ // if let date = self.convertToDate(dateString: createdAt) {
504
+ // formattedDate = self.formatDate(date: date)
505
+ // } else {
506
+ // formattedDate = "N/A"
507
+ // }
508
+ //
509
+ // self.lblChargeId.text = chargeId
510
+ // self.lblTransactionId.text = transactionId
511
+ // self.lblSubscriptionId.text = subscriptionId
512
+ // self.lblDate.text = formattedDate
513
+ // // self.lblTotal.text = "$\(self.amount ?? 0)"
514
+ // let rawAmount = Double(self.request?.amount ?? 0)
515
+ // let amountText = String(format: "$%.2f", rawAmount)
516
+ // self.lblTotal.text = "\(amountText)"
517
+ //
518
+ // self.lblRefferenceId.text = referenceId
519
+ // self.lblStatus.text = status
520
+ //
521
+ // if status.lowercased() == "failed" {
522
+ // self.imgViewPaymentDone.image = UIImage(named: "payment_error_icon", in: .easyPayBundle, compatibleWith: nil)
523
+ // self.imgViewPaymentDone.tintColor = .systemRed
524
+ // self.apiStatusCheckTimer?.invalidate()
525
+ // self.apiStatusCheckTimer = nil
526
+ // self.oneMinuteCountdownTimer?.cancel()
527
+ // self.oneMinuteCountdownTimer = nil
528
+ //
529
+ // self.imgViewLoading.layer.removeAllAnimations()
530
+ // self.imgViewLoading.isHidden = true
531
+ // self.imgViewPaymentDone.isHidden = false
532
+ // self.lblCompleteAuthentication.text = "Payment Failed"
533
+ // self.btnDone.isHidden = false
534
+ // // self.lblPaymentLink.isHidden = false
535
+ // self.viewPaymentDetails.isHidden = false
536
+ //
537
+ // self.btnContinue.isHidden = true
538
+ // }
539
+ // else if status.lowercased() == "completed" {
540
+ // // Success case
541
+ // self.imgViewPaymentDone.image = UIImage(named: "payment_done_icon", in: .easyPayBundle, compatibleWith: nil)
542
+ // self.apiStatusCheckTimer?.invalidate()
543
+ // self.apiStatusCheckTimer = nil
544
+ // self.oneMinuteCountdownTimer?.cancel()
545
+ // self.oneMinuteCountdownTimer = nil
546
+ //
547
+ // self.imgViewLoading.layer.removeAllAnimations()
548
+ // self.imgViewLoading.isHidden = true
549
+ // self.imgViewPaymentDone.isHidden = false
550
+ // self.lblCompleteAuthentication.text = "Authentication Completed"
551
+ // self.btnDone.isHidden = false
552
+ // // self.lblPaymentLink.isHidden = false
553
+ // self.viewPaymentDetails.isHidden = false
554
+ //
555
+ // self.btnContinue.isHidden = true
556
+ // } else {
557
+ // // Still waiting
558
+ // self.imgViewPaymentDone.image = UIImage(systemName: "exclamationmark.circle.fill")
559
+ // self.imgViewPaymentDone.tintColor = .systemRed
560
+ // }
561
+ // }
562
+ // }
563
+ // }
564
+ // } catch {
565
+ // }
566
+ // }
567
+ // task.resume()
568
+ // }
569
+ //
570
+ //}
571
+ //
572
+
573
+
574
+
575
+
576
+
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
+ import UIKit
614
+
615
+ class ThreeDSecurePaymentDoneVC: BaseVC {
616
+
617
+ @IBOutlet weak var imgViewPaymentDone: UIImageView!
618
+ @IBOutlet weak var imgViewLoading: UIImageView!
619
+ @IBOutlet weak var viewMain: UIView!
620
+ @IBOutlet weak var lblCompleteAuthentication: UILabel!
621
+ @IBOutlet weak var btnDone: UIButton!
622
+ @IBOutlet weak var lblBillingInfoData: UILabel!
623
+
624
+ @IBOutlet weak var viewPaymentDetails: UIStackView!
625
+ @IBOutlet weak var lblDateHeading: UILabel!
626
+ @IBOutlet weak var lblTransactionHeading: UILabel!
627
+ @IBOutlet weak var lblSubscriptionHeading: UILabel!
628
+ @IBOutlet weak var lblChargeIdHeading: UILabel!
629
+ @IBOutlet weak var lblReferenceIdHeading: UILabel!
630
+ @IBOutlet weak var lblPaymentMethodHeading: UILabel!
631
+ @IBOutlet weak var lblTotalHeading: UILabel!
632
+ @IBOutlet weak var lblStatusHeading: UILabel!
633
+
634
+ @IBOutlet weak var lblDate: UILabel!
635
+ @IBOutlet weak var lblTransactionId: UILabel!
636
+ @IBOutlet weak var lblSubscriptionId: UILabel!
637
+ @IBOutlet weak var lblChargeId: UILabel!
638
+ @IBOutlet weak var lblTotal: UILabel!
639
+ @IBOutlet weak var lblRefferenceId: UILabel!
640
+ @IBOutlet weak var lblPaymentMethodValue: UILabel!
641
+ @IBOutlet weak var lblStatus: UILabel!
642
+
643
+ @IBOutlet weak var btnContinue: UIButton!
644
+
645
+ var easyPayDelegate: EasyPayViewControllerDelegate?
646
+ var redirectURL: String?
647
+ var chargeData: [String: Any] = [:]
648
+
649
+ var billingInfo: [String: Any]?
650
+ var additionalInfo: [String: Any]?
651
+
652
+ var threeDSecureStatusResponse: [String: Any]?
653
+
654
+ var apiStatusCheckTimer: Timer?
655
+ var oneMinuteCountdownTimer: DispatchSourceTimer?
656
+ var countdownRemaining: Int = 180
657
+
658
+ var additionalInfoData: [FieldItem]?
659
+ var billingInfoData: [FieldItem]?
660
+ var visibility: FieldsVisibility?
661
+
662
+ let initialAPIGroup = DispatchGroup()
663
+
664
+ var amount: Double?
665
+
666
+ var cardApiParams: [String: Any]?
667
+
668
+ var request: Request!
669
+
670
+ var didTapContinue = false
671
+
672
+ override func viewDidLoad() {
673
+ super.viewDidLoad()
674
+ uiFinishingTouchElements()
675
+
676
+ // Initially hide done image and show loading image
677
+ imgViewPaymentDone.isHidden = true
678
+ imgViewLoading.isHidden = false
679
+ lblCompleteAuthentication.text = "Completing Authentication..."
680
+ btnDone.isHidden = true
681
+ btnContinue.isHidden = false
682
+
683
+ // Populate initial label values
684
+ populateInitialLabels()
685
+
686
+ startRotatingImage()
687
+ startOneMinuteCountdownAndAPICheck()
688
+
689
+ NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
690
+ }
691
+
692
+ deinit {
693
+ NotificationCenter.default.removeObserver(self)
694
+ }
695
+
696
+ @objc private func appDidBecomeActive() {
697
+ if !imgViewLoading.isHidden && imgViewLoading.layer.animation(forKey: "rotationAnimation") == nil {
698
+ startRotatingImage()
699
+ }
700
+ }
701
+
702
+ override func viewWillAppear(_ animated: Bool) {
703
+ super.viewWillAppear(animated)
704
+ uiFinishingTouchElements()
705
+
706
+ // Restart rotating animation if missing
707
+ if !imgViewLoading.isHidden && imgViewLoading.layer.animation(forKey: "rotationAnimation") == nil {
708
+ startRotatingImage()
709
+ }
710
+
711
+ if let chargeId = (threeDSecureStatusResponse?["data"] as? [String: Any])?["charge_id"] as? String,
712
+ !chargeId.isEmpty {
713
+ // Charge already found - do nothing
714
+ return
715
+ }
716
+
717
+ if countdownRemaining > 0 {
718
+ imgViewLoading.isHidden = false
719
+ imgViewPaymentDone.isHidden = true
720
+ if !didTapContinue {
721
+ lblCompleteAuthentication.text = "Tap Continue to proceed with 3D Secure authentication."
722
+ }
723
+ btnDone.isHidden = true
724
+ btnContinue.isHidden = false
725
+
726
+ if apiStatusCheckTimer == nil {
727
+ apiStatusCheckTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
728
+ guard let self = self else { return }
729
+ if self.countdownRemaining > 0 {
730
+ self.checkThreeDSecureStatus(completion: nil)
731
+ }
732
+ }
733
+ }
734
+ } else {
735
+ imgViewLoading.layer.removeAllAnimations()
736
+ imgViewLoading.isHidden = true
737
+ imgViewPaymentDone.isHidden = false
738
+ imgViewPaymentDone.image = UIImage(systemName: "exclamationmark.circle.fill")
739
+ imgViewPaymentDone.tintColor = .systemRed
740
+ if !didTapContinue {
741
+ lblCompleteAuthentication.text = "Tap Continue to proceed with 3D Secure authentication."
742
+ }
743
+ btnDone.isHidden = false
744
+ viewPaymentDetails.isHidden = false
745
+ btnContinue.isHidden = true
746
+
747
+ stopStatusTimers()
748
+ }
749
+ }
750
+
751
+ override func viewWillDisappear(_ animated: Bool) {
752
+ super.viewWillDisappear(animated)
753
+ // Match Android behavior: keep polling while user is in the 3DS WebView.
754
+ // Only stop timers when this VC is actually being dismissed/popped.
755
+ if isMovingFromParent || isBeingDismissed {
756
+ stopStatusTimers()
757
+ }
758
+ }
759
+
760
+ private func showHourglassGIF() {
761
+ guard let bundlePath = Bundle.easyPayBundle.path(forResource: "hourglass", ofType: "gif"),
762
+ let gifData = try? Data(contentsOf: URL(fileURLWithPath: bundlePath)),
763
+ let source = CGImageSourceCreateWithData(gifData as CFData, nil) else {
764
+ imgViewLoading.image = UIImage(systemName: "hourglass")?.withRenderingMode(.alwaysTemplate)
765
+ imgViewLoading.tintColor = UIColor.systemGray
766
+ imgViewLoading.contentMode = .scaleAspectFit
767
+ return
768
+ }
769
+
770
+ var images: [UIImage] = []
771
+ var duration: Double = 0
772
+
773
+ let frameCount = CGImageSourceGetCount(source)
774
+ for i in 0..<frameCount {
775
+ if let cgImage = CGImageSourceCreateImageAtIndex(source, i, nil) {
776
+ let frameDuration = getFrameDuration(from: source, index: i)
777
+ duration += frameDuration
778
+ images.append(UIImage(cgImage: cgImage))
779
+ }
780
+ }
781
+
782
+ if !images.isEmpty {
783
+ let animatedImage = UIImage.animatedImage(with: images, duration: duration)
784
+ imgViewLoading.image = animatedImage
785
+ imgViewLoading.contentMode = .scaleToFill
786
+ } else {
787
+ imgViewLoading.image = UIImage(systemName: "hourglass")?.withRenderingMode(.alwaysTemplate)
788
+ imgViewLoading.tintColor = UIColor.systemGray
789
+ imgViewLoading.contentMode = .scaleAspectFit
790
+ }
791
+ }
792
+
793
+ private func getFrameDuration(from source: CGImageSource, index: Int) -> Double {
794
+ let defaultFrameDuration = 0.1
795
+
796
+ guard let properties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [CFString: Any],
797
+ let gifProperties = properties[kCGImagePropertyGIFDictionary] as? [CFString: Any] else {
798
+ return defaultFrameDuration
799
+ }
800
+
801
+ if let unclampedDelay = gifProperties[kCGImagePropertyGIFUnclampedDelayTime] as? Double, unclampedDelay > 0 {
802
+ return unclampedDelay
803
+ }
804
+
805
+ if let delay = gifProperties[kCGImagePropertyGIFDelayTime] as? Double, delay > 0 {
806
+ return delay
807
+ }
808
+
809
+ return defaultFrameDuration
810
+ }
811
+
812
+ func startRotatingImage() {
813
+ showHourglassGIF()
814
+ }
815
+
816
+ @IBAction func actionBtnContinue(_ sender: UIButton) {
817
+ guard let urlStr = redirectURL, !urlStr.isEmpty else {
818
+ return
819
+ }
820
+
821
+ didTapContinue = true
822
+ lblCompleteAuthentication.text = "Waiting for 3DS Authentication..."
823
+ btnContinue.isHidden = true
824
+
825
+ let vc = EasyPaySdk.instantiateViewController(withIdentifier: "PaymentStatusWebViewVC") as! PaymentStatusWebViewVC
826
+ vc.urlString = urlStr
827
+ self.navigationController?.pushViewController(vc, animated: true)
828
+ }
829
+
830
+ private func populateInitialLabels() {
831
+ // Populate labels with initial data from chargeData or defaults
832
+ let refToken = chargeData["ref_token"] as? String ?? "N/A"
833
+ let chargeId = chargeData["charge_id"] as? String ?? "N/A"
834
+ let transactionId = chargeData["transaction_id"] as? String ?? "N/A"
835
+ let subscriptionId = chargeData["subscription_id"] as? String ?? "N/A"
836
+ let createdAt = chargeData["created_at"] as? String ?? ""
837
+ let status = chargeData["status"] as? String ?? "N/A"
838
+
839
+ let formattedDate: String
840
+ if let date = convertToDate(dateString: createdAt) {
841
+ formattedDate = formatDate(date: date)
842
+ } else {
843
+ formattedDate = "N/A"
844
+ }
845
+
846
+ lblChargeId.text = chargeId
847
+ lblTransactionId.text = transactionId
848
+ lblSubscriptionId.text = subscriptionId
849
+ lblDate.text = formattedDate
850
+ let rawAmount = Double(request?.amount ?? 0)
851
+ let amountText = String(format: "$%.2f", rawAmount)
852
+ lblTotal.text = amountText
853
+ lblRefferenceId.text = refToken
854
+ lblStatus.text = status
855
+ lblPaymentMethodValue.text = "Card" // Set to "Card" by default
856
+ }
857
+
858
+ private func startOneMinuteCountdownAndAPICheck() {
859
+ startRotatingImage()
860
+ imgViewPaymentDone.isHidden = true
861
+ imgViewLoading.isHidden = false
862
+ if !didTapContinue {
863
+ lblCompleteAuthentication.text = "Tap Continue to proceed with 3D Secure authentication."
864
+ }
865
+ btnDone.isHidden = true
866
+ btnContinue.isHidden = false
867
+
868
+ stopStatusTimers()
869
+
870
+ initialAPIGroup.enter()
871
+ checkThreeDSecureStatus { [weak self] in
872
+ self?.initialAPIGroup.leave()
873
+ }
874
+
875
+ countdownRemaining = 180
876
+
877
+ oneMinuteCountdownTimer = DispatchSource.makeTimerSource(queue: .main)
878
+ oneMinuteCountdownTimer?.schedule(deadline: .now(), repeating: .seconds(1))
879
+ oneMinuteCountdownTimer?.setEventHandler { [weak self] in
880
+ guard let self = self else { return }
881
+
882
+ self.countdownRemaining -= 1
883
+ if self.countdownRemaining <= 0 {
884
+ self.stopStatusTimers()
885
+
886
+ DispatchQueue.main.async {
887
+ self.imgViewLoading.layer.removeAllAnimations()
888
+ self.imgViewLoading.isHidden = true
889
+ self.imgViewPaymentDone.isHidden = false
890
+ self.imgViewPaymentDone.image = UIImage(systemName: "exclamationmark.circle.fill")
891
+ self.imgViewPaymentDone.tintColor = .systemRed
892
+ if !self.didTapContinue {
893
+ self.lblCompleteAuthentication.text = "Tap Continue to proceed with 3D Secure authentication."
894
+ }
895
+ self.btnDone.isHidden = false
896
+ self.viewPaymentDetails.isHidden = false
897
+ self.btnContinue.isHidden = true
898
+
899
+ if let json = self.threeDSecureStatusResponse,
900
+ let data = json["data"] as? [String: Any] {
901
+ let refToken = data["ref_token"] as? String ?? "N/A"
902
+ let chargeId = data["charge_id"] as? String ?? "N/A"
903
+ let transactionId = data["transaction_id"] as? String ?? "N/A"
904
+ let subscriptionId = data["subscription_id"] as? String ?? "N/A"
905
+ let createdAt = data["created_at"] as? String ?? ""
906
+
907
+ let formattedDate: String
908
+ if let date = self.convertToDate(dateString: createdAt) {
909
+ formattedDate = self.formatDate(date: date)
910
+ } else {
911
+ formattedDate = "N/A"
912
+ }
913
+
914
+ self.lblChargeId.text = chargeId
915
+ self.lblTransactionId.text = transactionId
916
+ self.lblSubscriptionId.text = subscriptionId
917
+ self.lblDate.text = formattedDate
918
+ let rawAmount = Double(self.request?.amount ?? 0)
919
+ let amountText = String(format: "$%.2f", rawAmount)
920
+ self.lblTotal.text = amountText
921
+ self.lblRefferenceId.text = refToken
922
+ self.lblPaymentMethodValue.text = "Card"
923
+ }
924
+ // Match Android: timeout shows warning UI only.
925
+ }
926
+ }
927
+ }
928
+ oneMinuteCountdownTimer?.resume()
929
+
930
+ apiStatusCheckTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
931
+ guard let self = self else { return }
932
+ self.checkThreeDSecureStatus(completion: nil)
933
+ }
934
+ }
935
+
936
+ @IBAction func actionBtnDone(_ sender: UIButton) {
937
+ var resultBillingInfo: [String: Any]? = nil
938
+
939
+ if let cardNumber = cardApiParams?["card_number"] as? String, cardNumber.count >= 4 {
940
+ let last4 = String(cardNumber.suffix(4))
941
+ let masked = "****\(last4)"
942
+ chargeData["card_number_last_4"] = masked
943
+ }
944
+
945
+ if let billing = billingInfo, !billing.isEmpty {
946
+ resultBillingInfo = billing
947
+ }
948
+
949
+ var response: [String: Any] = [:]
950
+
951
+ if let threeDS = threeDSecureStatusResponse {
952
+ for (key, value) in threeDS {
953
+ response[key] = value
954
+ }
955
+ } else {
956
+ for (key, value) in chargeData {
957
+ response[key] = value
958
+ }
959
+ }
960
+
961
+ if let billing = resultBillingInfo {
962
+ response["billingInfo"] = billing
963
+ }
964
+
965
+ if let additional = additionalInfo {
966
+ response["additionalInfo"] = additional
967
+ }
968
+
969
+ if let last4 = chargeData["card_number_last_4"] as? String, response["card_number_last_4"] == nil {
970
+ response["card_number_last_4"] = last4
971
+ }
972
+
973
+ let result = SDKResult(type: .success, data: response as NSDictionary)
974
+
975
+ easyPayDelegate?.easyPayController(self.navigationController as! EasyPayViewController, didFinishWith: result)
976
+
977
+ if let easyPayVC = self.navigationController as? EasyPayViewController {
978
+ easyPayVC.dismiss(animated: true, completion: {
979
+ if let delegate = easyPayVC.easyPayDelegate {
980
+ UserStoreSingleton.shared.isLoggedIn = false
981
+ delegate.easyPayController(easyPayVC, didFinishWith: result)
982
+ }
983
+ })
984
+ }
985
+ }
986
+
987
+ func uiFinishingTouchElements() {
988
+ if let containerBGcolor = UserStoreSingleton.shared.container_bg_col,
989
+ let uiColor = UIColor(hex: containerBGcolor) {
990
+ self.view.backgroundColor = uiColor
991
+ self.viewMain.backgroundColor = uiColor
992
+ self.imgViewLoading.backgroundColor = uiColor
993
+ }
994
+
995
+ if let primaryBtnBackGroundColor = UserStoreSingleton.shared.primary_btn_bg_col,
996
+ let uiColor = UIColor(hex: primaryBtnBackGroundColor) {
997
+ btnDone.backgroundColor = uiColor
998
+ // Removed imgViewLoading.image assignment to avoid overriding GIF
999
+ imgViewLoading.tintColor = uiColor
1000
+ btnContinue.backgroundColor = uiColor
1001
+ }
1002
+
1003
+ if let primaryBtnFontColor = UserStoreSingleton.shared.primary_btn_font_col,
1004
+ let secondaryUIColor = UIColor(hex: primaryBtnFontColor) {
1005
+ btnDone.setTitleColor(secondaryUIColor, for: .normal)
1006
+ btnContinue.setTitleColor(secondaryUIColor, for: .normal)
1007
+ }
1008
+
1009
+ if let primaryFontColor = UserStoreSingleton.shared.primary_font_col,
1010
+ let uiColor = UIColor(hex: primaryFontColor) {
1011
+ lblDateHeading.textColor = uiColor
1012
+ lblTransactionHeading.textColor = uiColor
1013
+ lblSubscriptionHeading.textColor = uiColor
1014
+ lblChargeIdHeading.textColor = uiColor
1015
+ lblReferenceIdHeading.textColor = uiColor
1016
+ lblPaymentMethodHeading.textColor = uiColor
1017
+ lblTotalHeading.textColor = uiColor
1018
+ lblDate.textColor = uiColor
1019
+ lblTransactionId.textColor = uiColor
1020
+ lblSubscriptionId.textColor = uiColor
1021
+ lblChargeId.textColor = uiColor
1022
+ lblTotal.textColor = uiColor
1023
+ lblRefferenceId.textColor = uiColor
1024
+ lblPaymentMethodValue.textColor = uiColor
1025
+ lblStatusHeading.textColor = uiColor
1026
+ lblStatus.textColor = uiColor
1027
+ }
1028
+
1029
+ if let borderRadiusString = UserStoreSingleton.shared.border_radious,
1030
+ let borderRadius = Double(borderRadiusString) {
1031
+ btnDone.layer.cornerRadius = CGFloat(borderRadius)
1032
+ } else {
1033
+ btnDone.layer.cornerRadius = 8
1034
+ }
1035
+ btnDone.layer.masksToBounds = true
1036
+
1037
+ if let primaryFontColor = UserStoreSingleton.shared.primary_font_col,
1038
+ let uiColor = UIColor(hex: primaryFontColor) {
1039
+ lblCompleteAuthentication.textColor = uiColor
1040
+ lblBillingInfoData.textColor = uiColor
1041
+ }
1042
+
1043
+ if let fontSizeString = UserStoreSingleton.shared.fontSize,
1044
+ let fontSizeDouble = Double(fontSizeString) {
1045
+ let fontSize = CGFloat(fontSizeDouble)
1046
+ lblBillingInfoData.font = UIFont.systemFont(ofSize: fontSize, weight: .medium)
1047
+ }
1048
+ }
1049
+
1050
+ private func convertToDate(dateString: String) -> Date? {
1051
+ let formatter = DateFormatter()
1052
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
1053
+ formatter.timeZone = TimeZone(abbreviation: "UTC")
1054
+ return formatter.date(from: dateString)
1055
+ }
1056
+
1057
+ private func formatDate(date: Date) -> String {
1058
+ let formatter = DateFormatter()
1059
+ formatter.dateFormat = "dd/MM/yyyy, HH:mm:ss"
1060
+ formatter.timeZone = TimeZone.current
1061
+ return formatter.string(from: date)
1062
+ }
1063
+
1064
+ private func checkThreeDSecureStatus(completion: (() -> Void)?) {
1065
+ // Status API accepts secure_token or ref_token (same path: 3dsecure/{token}/status).
1066
+ // Resolve from top-level or nested data (match Android: init response may have ref_token in either place).
1067
+ let dataDict = chargeData["data"] as? [String: Any]
1068
+ let statusToken = (chargeData["secure_token"] as? String)
1069
+ ?? (chargeData["ref_token"] as? String)
1070
+ ?? (dataDict?["secure_token"] as? String)
1071
+ ?? (dataDict?["ref_token"] as? String)
1072
+ guard let token = statusToken, !token.isEmpty else {
1073
+ completion?()
1074
+ return
1075
+ }
1076
+
1077
+ let urlString = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.threeDSecureStatus(token).path()
1078
+
1079
+ guard let url = URL(string: urlString) else {
1080
+ completion?()
1081
+ return
1082
+ }
1083
+
1084
+ var uRLRequest = URLRequest(url: url)
1085
+ uRLRequest.httpMethod = "GET"
1086
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
1087
+
1088
+ let clientToken = UserStoreSingleton.shared.clientToken ?? ""
1089
+ uRLRequest.addValue(clientToken, forHTTPHeaderField: "Client-Token")
1090
+
1091
+ let task = URLSession.shared.dataTask(with: uRLRequest) { [weak self] data, response, error in
1092
+ defer { completion?() }
1093
+
1094
+ guard let self = self else { return }
1095
+
1096
+ if let error = error {
1097
+ return
1098
+ }
1099
+
1100
+ guard let data = data else {
1101
+ return
1102
+ }
1103
+
1104
+ do {
1105
+ if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
1106
+ self.threeDSecureStatusResponse = json
1107
+
1108
+ if let dataDict = json["data"] as? [String: Any] {
1109
+ DispatchQueue.main.async {
1110
+ let chargeId = dataDict["charge_id"] as? String ?? "N/A"
1111
+ let transactionId = dataDict["transaction_id"] as? String ?? "N/A"
1112
+ let subscriptionId = dataDict["subscription_id"] as? String ?? "N/A"
1113
+ let createdAt = dataDict["created_at"] as? String ?? ""
1114
+ let referenceId = dataDict["ref_token"] as? String ?? "N/A"
1115
+ let status = dataDict["status"] as? String ?? "N/A"
1116
+
1117
+ let formattedDate: String
1118
+ if let date = self.convertToDate(dateString: createdAt) {
1119
+ formattedDate = self.formatDate(date: date)
1120
+ } else {
1121
+ formattedDate = "N/A"
1122
+ }
1123
+
1124
+ self.lblChargeId.text = chargeId
1125
+ self.lblTransactionId.text = transactionId
1126
+ self.lblSubscriptionId.text = subscriptionId
1127
+ self.lblDate.text = formattedDate
1128
+ let rawAmount = Double(self.request?.amount ?? 0)
1129
+ let amountText = String(format: "$%.2f", rawAmount)
1130
+ self.lblTotal.text = amountText
1131
+ self.lblRefferenceId.text = referenceId
1132
+ self.lblStatus.text = status
1133
+ self.lblPaymentMethodValue.text = "Card" // Ensure "Card" in all cases
1134
+
1135
+ // MATCH ANDROID EXACTLY:
1136
+ // - keep polling ONLY while status == "initiated"
1137
+ // - stop polling and finalize UI for ANY other status
1138
+ let normalizedStatus = status.lowercased()
1139
+ if normalizedStatus == "initiated" {
1140
+ return
1141
+ }
1142
+
1143
+ self.stopStatusTimers()
1144
+
1145
+ self.imgViewLoading.layer.removeAllAnimations()
1146
+ self.imgViewLoading.isHidden = true
1147
+ self.imgViewPaymentDone.isHidden = false
1148
+ self.btnDone.isHidden = false
1149
+ self.viewPaymentDetails.isHidden = false
1150
+ self.btnContinue.isHidden = true
1151
+
1152
+ if normalizedStatus == "failed" {
1153
+ self.imgViewPaymentDone.image = UIImage(named: "payment_error_icon", in: .easyPayBundle, compatibleWith: nil)
1154
+ self.imgViewPaymentDone.tintColor = .systemRed
1155
+ self.lblCompleteAuthentication.text = "Transaction Failed.!"
1156
+ } else if normalizedStatus == "expired" {
1157
+ self.imgViewPaymentDone.image = UIImage(systemName: "exclamationmark.circle.fill")
1158
+ self.imgViewPaymentDone.tintColor = .systemRed
1159
+ self.lblCompleteAuthentication.text = "Authentication Expired"
1160
+ } else {
1161
+ self.imgViewPaymentDone.image = UIImage(named: "payment_done_icon", in: .easyPayBundle, compatibleWith: nil)
1162
+ self.imgViewPaymentDone.tintColor = nil
1163
+ self.lblCompleteAuthentication.text = "Authentication Completed"
1164
+ }
1165
+ }
1166
+ }
1167
+ }
1168
+ } catch {
1169
+ }
1170
+ }
1171
+ task.resume()
1172
+ }
1173
+
1174
+ private func stopStatusTimers() {
1175
+ apiStatusCheckTimer?.invalidate()
1176
+ apiStatusCheckTimer = nil
1177
+ oneMinuteCountdownTimer?.cancel()
1178
+ oneMinuteCountdownTimer = nil
1179
+ }
1180
+ }
1181
+
1182
+
1183
+
1184
+
1185
+
1186
+
1187
+
1188
+
1189
+
1190
+
1191
+
1192
+
1193
+
1194
+
1195
+
1196
+
1197
+
1198
+
1199
+
1200
+
1201
+
1202
+
1203
+
1204
+
1205
+
1206
+
1207
+
1208
+
1209
+
1210
+
1211
+
1212
+
1213
+
1214
+
1215
+
1216
+