@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,3686 @@
1
+ //
2
+ // BillingInfoVC.swift
3
+ // EasyPay
4
+ //
5
+ // Created by Mony's Mac on 12/08/24.
6
+ //
7
+
8
+ import UIKit
9
+
10
+ @available(iOS 16.0, *)
11
+
12
+ protocol BillingInfoVCDelegate: AnyObject {
13
+ func didPassTextBack(_ text: String)
14
+ }
15
+
16
+ class BillingInfoVC: BaseVC {
17
+
18
+ // @IBOutlet weak var viewBillingInfo: UIView!
19
+ @IBOutlet weak var btnPrevious: UIButton!
20
+ @IBOutlet weak var viewCountryList: UIView!
21
+ @IBOutlet weak var searchBarCountryList: UISearchBar!
22
+ @IBOutlet weak var tblViewCountryList: UITableView!
23
+ @IBOutlet weak var txtFieldAddress: UITextField!
24
+ @IBOutlet weak var txtFieldCountry: UITextField!
25
+ @IBOutlet weak var viewStateList: UIView!
26
+ @IBOutlet weak var searchBarStateList: UISearchBar!
27
+ @IBOutlet weak var tblViewStateList: UITableView!
28
+ @IBOutlet weak var txtFieldState: UITextField!
29
+ @IBOutlet weak var viewCityList: UIView!
30
+ @IBOutlet weak var tblViewCityList: UITableView!
31
+ @IBOutlet weak var txtFieldCity: UITextField!
32
+ @IBOutlet weak var txtFieldPostalCode: UITextField!
33
+
34
+ @IBOutlet weak var buttonBottomConstraint: NSLayoutConstraint!
35
+
36
+ @IBOutlet weak var lblBillingInfo: UILabel!
37
+ @IBOutlet weak var lblEasyMerchant: UILabel!
38
+ @IBOutlet weak var btnNext: UIButton!
39
+
40
+ @IBOutlet weak var lblStarAddressField: UILabel!
41
+ @IBOutlet weak var lblStarCountryField: UILabel!
42
+ @IBOutlet weak var lblStarStateField: UILabel!
43
+ @IBOutlet weak var lblStarCityField: UILabel!
44
+ @IBOutlet weak var lblStarPostalCodeField: UILabel!
45
+
46
+ @IBOutlet weak var btnSelectState: UIButton!
47
+ @IBOutlet weak var btnSelectCity: UIButton!
48
+
49
+ // Variable to store the auth token
50
+ var authToken: String?
51
+
52
+ var countryList: [[String: Any]] = []
53
+ var stateList: [[String: Any]] = []
54
+ var cityList: [[String: Any]] = []
55
+
56
+ // var billingInfoData: [String: Any]?
57
+ var billingInfoData: Data?
58
+
59
+ var cardNumber: String?
60
+ var expiryDate: String?
61
+ var cvv: String?
62
+ var nameOnCard: String?
63
+ var userEmail: String?
64
+
65
+ //Banking Params
66
+ var accountName: String?
67
+ var routingNumber: String?
68
+ var accountType: String?
69
+ var accountNumber: String?
70
+
71
+ var selectedPaymentMethod: String?
72
+
73
+ private let keyboardObserver = KeyboardObserver()
74
+
75
+ var isSavedForFuture: Bool = false
76
+ var isSavedNewCard: Bool = false
77
+
78
+ var request: Request!
79
+ var selectedCard: CardModel?
80
+ // var amount: Int?
81
+ var amount: Double?
82
+ var cvvText: String?
83
+
84
+ var isFrom = String()
85
+ var isFromm = String()
86
+
87
+ //From Regular Saved Bank Accounts
88
+ var customerID: String?
89
+ var accountID: String?
90
+
91
+ var isSavedNewAccount: Bool?
92
+
93
+ weak var delegate: BillingInfoVCDelegate?
94
+
95
+ var chosenPlan: String?
96
+ var startDate: String?
97
+
98
+ //GrailPay Params
99
+ var grailPayAccountID: String?
100
+ var selectedGrailPayAccountType: String?
101
+ var selectedGrailPayAccountName: String?
102
+
103
+ var billingInfo: [FieldItem]?
104
+ var additionalInfo: [FieldItem]?
105
+ var visibility: FieldsVisibility?
106
+ var fieldSection: FieldSection?
107
+
108
+ var easyPayDelegate: EasyPayViewControllerDelegate?
109
+
110
+ var filteredCountryList: [[String: Any]] = []
111
+ var isSearching = false
112
+
113
+ var filteredStateList: [[String: Any]] = [] // For search results
114
+ var isSearchingState = false // Track if user is searching
115
+
116
+ private var didApplyInitialCountry = false
117
+
118
+ override func viewDidLoad() {
119
+ super.viewDidLoad()
120
+ self.lblEasyMerchant.text = "POWERED BY \(UserStoreSingleton.shared.companyName?.uppercased() ?? "")"
121
+
122
+ updateNextButtonTitle()
123
+ btnSelectState.isHidden = true
124
+ btnSelectCity.isHidden = true
125
+
126
+ uiFinishingTouchElements()
127
+ // configureFieldVisibility()
128
+ setupShadowForListViews()
129
+
130
+ tblViewCountryList.delegate = self
131
+ tblViewCountryList.dataSource = self
132
+ viewCountryList.isHidden = true
133
+
134
+ tblViewStateList.delegate = self
135
+ tblViewStateList.dataSource = self
136
+ viewStateList.isHidden = true
137
+
138
+ tblViewCityList.delegate = self
139
+ tblViewCityList.dataSource = self
140
+ viewCityList.isHidden = true
141
+
142
+ txtFieldCity.delegate = self
143
+ txtFieldState.delegate = self
144
+ txtFieldAddress.delegate = self
145
+ txtFieldCountry.delegate = self
146
+ txtFieldPostalCode.delegate = self
147
+
148
+ searchBarCountryList.delegate = self
149
+ searchBarStateList.delegate = self
150
+
151
+ if let textField = searchBarCountryList.value(forKey: "searchField") as? UITextField {
152
+ textField.clearButtonMode = .never
153
+ }
154
+
155
+ if let textField = searchBarStateList.value(forKey: "searchField") as? UITextField {
156
+ textField.clearButtonMode = .never
157
+ }
158
+
159
+ getCountryListApi()
160
+
161
+ // Add tap gesture to hide the views and dismiss the keyboard
162
+ let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapOutside))
163
+ tapGesture.cancelsTouchesInView = false
164
+ self.view.addGestureRecognizer(tapGesture)
165
+
166
+ guard let billingInfoData = billingInfoData else {
167
+ return
168
+ }
169
+
170
+ do {
171
+ let decodedFieldSection = try JSONDecoder().decode(FieldSection.self, from: billingInfoData)
172
+ self.fieldSection = decodedFieldSection // <--- Assign here!
173
+
174
+ // Fill Billing Fields UI
175
+ for item in decodedFieldSection.billing {
176
+ switch item.name {
177
+ case "address":
178
+ txtFieldAddress.text = item.value
179
+ case "country":
180
+ txtFieldCountry.text = item.value
181
+ case "state":
182
+ txtFieldState.text = item.value
183
+ case "city":
184
+ txtFieldCity.text = item.value
185
+ case "postal_code":
186
+ txtFieldPostalCode.text = item.value
187
+ default:
188
+ break
189
+ }
190
+ }
191
+ // Now that fieldSection is set, update star labels visibility
192
+ configureFieldVisibility()
193
+
194
+ } catch {
195
+ }
196
+
197
+ }
198
+
199
+ override func viewWillAppear(_ animated: Bool) {
200
+ super.viewWillAppear(animated)
201
+ updateNextButtonTitle()
202
+ uiFinishingTouchElements()
203
+
204
+ keyboardObserver.animateChanges({ [self] height in
205
+ let newConstant = CGFloat.maximum(height - self.view.safeAreaInsets.bottom + 8, 8)
206
+ buttonBottomConstraint.constant = newConstant
207
+ self.view.setNeedsLayout()
208
+ self.view.layoutIfNeeded()
209
+ })
210
+
211
+ configureFieldVisibility()
212
+ }
213
+
214
+ override func viewDidAppear(_ animated: Bool) {
215
+ super.viewDidAppear(animated)
216
+
217
+ if !didApplyInitialCountry {
218
+ didApplyInitialCountry = true
219
+ handleCountrySelection(countryName: txtFieldCountry.text ?? "")
220
+ }
221
+ }
222
+
223
+ override func viewWillDisappear(_ animated: Bool) {
224
+ super.viewWillDisappear(animated)
225
+ keyboardObserver.invalidate()
226
+ }
227
+
228
+ private func updateNextButtonTitle() {
229
+ guard let request = request else { return }
230
+
231
+ if let billingInfoData = request.fields,
232
+ let fieldSection = try? JSONDecoder().decode(FieldSection.self, from: billingInfoData) {
233
+
234
+ let isAdditionalVisible = fieldSection.visibility.additional
235
+ let isBillingVisible = fieldSection.visibility.billing
236
+ let amountText = String(format: "$%.2f", request.amount ?? 0)
237
+ let submitText = request.submitButtonText ?? "Submit"
238
+
239
+ if !isAdditionalVisible {
240
+ // Only billing info is visible
241
+ btnNext.setTitle("\(submitText) (\(amountText))", for: .normal)
242
+ } else {
243
+ // Additional info is visible
244
+ let suffix = isBillingVisible ? "(Additional Info)" : ""
245
+ btnNext.setTitle("\(submitText) \(suffix)", for: .normal)
246
+ }
247
+ } else {
248
+ let amountValue = request.amount ?? 0
249
+ let amountText = String(format: "$%.2f", amountValue)
250
+ let submitText = request.submitButtonText
251
+
252
+ let defaultTitle = (submitText?.isEmpty == false)
253
+ ? "\(submitText!) (\(amountText))"
254
+ : "Pay Now (\(amountText))"
255
+
256
+ btnNext.setTitle(defaultTitle, for: .normal)
257
+ }
258
+ }
259
+
260
+ func uiFinishingTouchElements() {
261
+ // Set background color for the main view
262
+ if let containerBGcolor = UserStoreSingleton.shared.container_bg_col,
263
+ let uiColor = UIColor(hex: containerBGcolor) {
264
+ self.view.backgroundColor = uiColor
265
+ viewCountryList.backgroundColor = uiColor
266
+ viewStateList.backgroundColor = uiColor
267
+ viewCityList.backgroundColor = uiColor
268
+ tblViewCountryList.backgroundColor = uiColor
269
+ tblViewStateList.backgroundColor = uiColor
270
+ tblViewCityList.backgroundColor = uiColor
271
+
272
+ self.searchBarCountryList.backgroundColor = uiColor
273
+ self.searchBarCountryList.barTintColor = uiColor
274
+ self.searchBarStateList.backgroundColor = uiColor
275
+ }
276
+
277
+ if let primaryBtnBackGroundColor = UserStoreSingleton.shared.primary_btn_bg_col,
278
+ let uiColor = UIColor(hex: primaryBtnBackGroundColor) {
279
+ btnNext.backgroundColor = uiColor
280
+ btnPrevious.setTitleColor(uiColor, for: .normal)
281
+ btnPrevious.layer.borderColor = uiColor.cgColor
282
+
283
+ searchBarCountryList.tintColor = uiColor
284
+ searchBarStateList.tintColor = uiColor
285
+ }
286
+
287
+ if let primaryBtnFontColor = UserStoreSingleton.shared.primary_btn_font_col,
288
+ let secondaryUIColor = UIColor(hex: primaryBtnFontColor) {
289
+ btnNext.setTitleColor(secondaryUIColor, for: .normal)
290
+ }
291
+
292
+ if let secondaryFontColor = UserStoreSingleton.shared.secondary_font_col,
293
+ let placeholderColor = UIColor(hex: secondaryFontColor) {
294
+ lblEasyMerchant.textColor = placeholderColor
295
+
296
+ // Set placeholder text color
297
+ let placeholderAttributes: [NSAttributedString.Key: Any] = [
298
+ .foregroundColor: placeholderColor
299
+ ]
300
+ searchBarCountryList.searchTextField.attributedPlaceholder = NSAttributedString(
301
+ string: searchBarCountryList.searchTextField.placeholder ?? "Search here",
302
+ attributes: placeholderAttributes
303
+ )
304
+
305
+ searchBarStateList.searchTextField.attributedPlaceholder = NSAttributedString(
306
+ string: searchBarStateList.searchTextField.placeholder ?? "Search here",
307
+ attributes: placeholderAttributes
308
+ )
309
+
310
+ // Set search icon color
311
+ searchBarCountryList.searchTextField.leftView?.tintColor = placeholderColor
312
+ searchBarStateList.searchTextField.leftView?.tintColor = placeholderColor
313
+ }
314
+
315
+ if let borderRadiusString = UserStoreSingleton.shared.border_radious,
316
+ let borderRadius = Double(borderRadiusString) { // Convert String to Double
317
+ btnNext.layer.cornerRadius = CGFloat(borderRadius) // Set corner radius
318
+ btnPrevious.layer.cornerRadius = CGFloat(borderRadius)
319
+ btnPrevious.layer.borderWidth = 1
320
+ } else {
321
+ btnNext.layer.cornerRadius = 8 // Default value
322
+ btnPrevious.layer.cornerRadius = 8
323
+ }
324
+ btnNext.layer.masksToBounds = true // Ensure the corners are clipped properly
325
+ btnPrevious.layer.masksToBounds = true
326
+
327
+ if let primaryFontColor = UserStoreSingleton.shared.primary_font_col,
328
+ let uiColor = UIColor(hex: primaryFontColor) {
329
+ lblBillingInfo.textColor = uiColor
330
+
331
+ searchBarCountryList.searchTextField.textColor = uiColor
332
+ searchBarStateList.searchTextField.textColor = uiColor
333
+ }
334
+
335
+ if let fontSizeString = UserStoreSingleton.shared.fontSize,
336
+ let fontSizeDouble = Double(fontSizeString) { // Convert String to Double
337
+ let fontSize = CGFloat(fontSizeDouble) // Convert Double to CGFloat
338
+ lblEasyMerchant.font = UIFont.systemFont(ofSize: fontSize)
339
+ btnNext.titleLabel?.font = UIFont.systemFont(ofSize: fontSize)
340
+ btnPrevious.titleLabel?.font = UIFont.systemFont(ofSize: fontSize)
341
+
342
+ searchBarCountryList.searchTextField.font = UIFont.systemFont(ofSize: fontSize)
343
+ searchBarStateList.searchTextField.font = UIFont.systemFont(ofSize: fontSize)
344
+ }
345
+
346
+ if let searchBar = self.searchBarCountryList {
347
+ searchBar.backgroundImage = UIImage() // Removes background line
348
+ searchBar.layer.borderWidth = 0
349
+ searchBar.layer.borderColor = UIColor.clear.cgColor
350
+ }
351
+
352
+ if let searchBar = self.searchBarStateList {
353
+ searchBar.backgroundImage = UIImage() // Removes background line
354
+ searchBar.layer.borderWidth = 0
355
+ searchBar.layer.borderColor = UIColor.clear.cgColor
356
+ }
357
+ }
358
+
359
+ private func getFieldValue(for name: String) -> String? {
360
+ return fieldSection?.billing.first(where: { $0.name == name })?.value
361
+ }
362
+
363
+ private func setFieldValue(_ name: String, to value: String?) {
364
+ guard let index = fieldSection?.billing.firstIndex(where: { $0.name == name }) else { return }
365
+ fieldSection?.billing[index].value = value ?? ""
366
+ }
367
+
368
+ private func configureFieldVisibility() {
369
+ guard let billingFields = fieldSection?.billing else {
370
+ return
371
+ }
372
+
373
+ func isFieldRequired(_ name: String) -> Bool {
374
+ let required = billingFields.first(where: { $0.name == name })?.required == true
375
+ return required
376
+ }
377
+
378
+ DispatchQueue.main.async {
379
+ self.lblStarAddressField.isHidden = !isFieldRequired("address")
380
+ self.lblStarCountryField.isHidden = !isFieldRequired("country")
381
+ self.lblStarStateField.isHidden = !isFieldRequired("state")
382
+ self.lblStarCityField.isHidden = !isFieldRequired("city")
383
+ self.lblStarPostalCodeField.isHidden = !isFieldRequired("postal_code")
384
+ self.view.layoutIfNeeded()
385
+ }
386
+ }
387
+
388
+ @objc func didTapOutside() {
389
+ // Dismiss the keyboard
390
+ self.view.endEditing(true)
391
+
392
+ // Hide the country list view if it is visible
393
+ if !viewCountryList.isHidden {
394
+ viewCountryList.isHidden = true
395
+ }
396
+
397
+ // Hide the state list view if it is visible
398
+ if !viewStateList.isHidden {
399
+ viewStateList.isHidden = true
400
+ }
401
+
402
+ // Hide the city list view if it is visible
403
+ if !viewCityList.isHidden {
404
+ viewCityList.isHidden = true
405
+ }
406
+ }
407
+
408
+ func setupShadowForListViews() {
409
+ // Ensure the view does not clip its content or shadows
410
+ viewCountryList.clipsToBounds = false
411
+ // Apply shadow properties
412
+ viewCountryList.layer.shadowColor = UIColor.black.cgColor
413
+ viewCountryList.layer.shadowOpacity = 0.3
414
+ viewCountryList.layer.shadowOffset = CGSize(width: 0, height: 2)
415
+ viewCountryList.layer.shadowRadius = 4
416
+ // Optionally, you might want to set a corner radius to the view as well
417
+ viewCountryList.layer.cornerRadius = 8
418
+ tblViewCountryList.layer.cornerRadius = 8
419
+
420
+ // Ensure the view does not clip its content or shadows
421
+ viewStateList.clipsToBounds = false
422
+ // Apply shadow properties
423
+ viewStateList.layer.shadowColor = UIColor.black.cgColor
424
+ viewStateList.layer.shadowOpacity = 0.3
425
+ viewStateList.layer.shadowOffset = CGSize(width: 0, height: 2)
426
+ viewStateList.layer.shadowRadius = 4
427
+ // Optionally, you might want to set a corner radius to the view as well
428
+ viewStateList.layer.cornerRadius = 8
429
+ tblViewStateList.layer.cornerRadius = 8
430
+
431
+ // Ensure the view does not clip its content or shadows
432
+ viewCityList.clipsToBounds = false
433
+ // Apply shadow properties
434
+ viewCityList.layer.shadowColor = UIColor.black.cgColor
435
+ viewCityList.layer.shadowOpacity = 0.3
436
+ viewCityList.layer.shadowOffset = CGSize(width: 0, height: 2)
437
+ viewCityList.layer.shadowRadius = 4
438
+ // Optionally, you might want to set a corner radius to the view as well
439
+ viewCityList.layer.cornerRadius = 8
440
+ tblViewCityList.layer.cornerRadius = 8
441
+ }
442
+
443
+ //MARK: Get Country List
444
+ func getCountryListApi() {
445
+ let session = URLSession.shared
446
+ let serviceURL = URL(string: "https://countriesnow.space/api/v0.1/countries/iso")!
447
+
448
+ var request = URLRequest(url: serviceURL)
449
+ request.httpMethod = "GET"
450
+
451
+ let task = session.dataTask(with: request) { (serviceData, serviceResponse, error) in
452
+ if let error = error {
453
+ self.hideLoadingIndicator()
454
+ return
455
+ }
456
+
457
+ guard let httpResponse = serviceResponse as? HTTPURLResponse, httpResponse.statusCode == 200 else {
458
+ self.hideLoadingIndicator()
459
+ return
460
+ }
461
+
462
+ guard let data = serviceData else {
463
+ self.hideLoadingIndicator()
464
+ return
465
+ }
466
+
467
+ do {
468
+ if let jsonResponse = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any],
469
+ let countryData = jsonResponse["data"] as? [[String: Any]] {
470
+
471
+ self.countryList = countryData // Store the list of countries
472
+
473
+ DispatchQueue.main.async {
474
+ self.tblViewCountryList.reloadData() // Reload the table view
475
+ }
476
+ } else {
477
+ self.hideLoadingIndicator()
478
+ }
479
+ } catch {
480
+ self.hideLoadingIndicator()
481
+ }
482
+ }
483
+ task.resume()
484
+ }
485
+
486
+ ///**In case of state and city name not found
487
+ func getStateListApi(for country: String) {
488
+ let urlString = "https://countriesnow.space/api/v0.1/countries/states"
489
+ guard let serviceURL = URL(string: urlString) else {
490
+ hideLoadingIndicator()
491
+ return
492
+ }
493
+
494
+ var request = URLRequest(url: serviceURL)
495
+ request.httpMethod = "POST"
496
+ request.addValue("application/json", forHTTPHeaderField: "Content-Type")
497
+
498
+ let params: [String: Any] = [
499
+ "country": country
500
+ ]
501
+
502
+ do {
503
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
504
+ request.httpBody = jsonData
505
+ } catch {
506
+ return
507
+ }
508
+
509
+ let session = URLSession.shared
510
+ let task = session.dataTask(with: request) { (serviceData, serviceResponse, error) in
511
+ if let error = error {
512
+ return
513
+ }
514
+
515
+ guard let httpResponse = serviceResponse as? HTTPURLResponse, httpResponse.statusCode == 200 else {
516
+ return
517
+ }
518
+
519
+ if let data = serviceData {
520
+ do {
521
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
522
+ let dataObject = responseObject["data"] as? [String: Any],
523
+ let statesArray = dataObject["states"] as? [[String: Any]] {
524
+
525
+ DispatchQueue.main.async {
526
+ self.stateList = statesArray
527
+ self.tblViewStateList.reloadData()
528
+
529
+ // If no state is found, clear the state and city fields and fill country in both
530
+ if statesArray.isEmpty {
531
+ self.txtFieldState.text = country
532
+ self.txtFieldCity.text = country
533
+ } else {
534
+ // Pick a random state and set it in txtFieldState
535
+ if let randomState = statesArray.randomElement(),
536
+ let stateName = randomState["name"] as? String {
537
+ self.txtFieldState.text = stateName
538
+
539
+ // Fetch cities for the randomly selected state and country
540
+ self.getCityListListApi(for: country, state: stateName)
541
+ }
542
+ }
543
+ }
544
+ } else {
545
+ }
546
+ } catch {
547
+ }
548
+ } else {
549
+ }
550
+ }
551
+ task.resume()
552
+ }
553
+
554
+ ///**In case of state and city name not found
555
+ func getCityListListApi(for country: String, state: String) {
556
+ let urlString = "https://countriesnow.space/api/v0.1/countries/state/cities"
557
+ guard let serviceURL = URL(string: urlString) else {
558
+ hideLoadingIndicator()
559
+ return
560
+ }
561
+
562
+ var request = URLRequest(url: serviceURL)
563
+ request.httpMethod = "POST"
564
+ request.addValue("application/json", forHTTPHeaderField: "Content-Type")
565
+
566
+ let params: [String: Any] = [
567
+ "country": country,
568
+ "state": state
569
+ ]
570
+
571
+ do {
572
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
573
+ request.httpBody = jsonData
574
+ } catch {
575
+ return
576
+ }
577
+
578
+ let session = URLSession.shared
579
+ let task = session.dataTask(with: request) { (serviceData, serviceResponse, error) in
580
+ if let error = error {
581
+ return
582
+ }
583
+
584
+ guard let httpResponse = serviceResponse as? HTTPURLResponse, httpResponse.statusCode == 200 else {
585
+ return
586
+ }
587
+
588
+ if let data = serviceData {
589
+ do {
590
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
591
+ let citiesArray = responseObject["data"] as? [String] { // Directly accessing city list array
592
+
593
+ DispatchQueue.main.async {
594
+ self.cityList = citiesArray.map { ["city_name": $0] } // Converting to expected format
595
+ self.tblViewCityList.reloadData()
596
+
597
+ // If no cities found, set state name in city field
598
+ if citiesArray.isEmpty {
599
+ self.txtFieldCity.text = self.txtFieldState.text
600
+ } else {
601
+ // Pick a random city and set it in txtFieldCity
602
+ if let randomCity = citiesArray.randomElement() {
603
+ self.txtFieldCity.text = randomCity
604
+ }
605
+ }
606
+ }
607
+ } else {
608
+ }
609
+ } catch {
610
+ }
611
+ } else {
612
+ }
613
+ }
614
+ task.resume()
615
+ }
616
+
617
+ func handleCountrySelection(countryName: String) {
618
+ var normalizedCountry = countryName
619
+ let lowercased = countryName.lowercased()
620
+
621
+ // Normalize common aliases
622
+ if lowercased == "usa" {
623
+ normalizedCountry = "United States"
624
+ }
625
+
626
+ if lowercased == "us" {
627
+ normalizedCountry = "United States"
628
+ }
629
+
630
+ // Show/hide state dropdown
631
+ if lowercased == "united states" || lowercased == "usa" || lowercased == "canada" {
632
+ btnSelectState.isHidden = false
633
+ txtFieldState.placeholder = "Select State"
634
+ } else {
635
+ btnSelectState.isHidden = true
636
+ txtFieldState.placeholder = "State"
637
+ }
638
+
639
+ // Fetch states using normalized country name
640
+ getStateListApi(for: normalizedCountry)
641
+ }
642
+
643
+ func updateBillingInfoData() {
644
+ setFieldValue("address", to: txtFieldAddress.text)
645
+ setFieldValue("country", to: txtFieldCountry.text)
646
+ setFieldValue("state", to: txtFieldState.text)
647
+ setFieldValue("city", to: txtFieldCity.text)
648
+ setFieldValue("postal_code", to: txtFieldPostalCode.text)
649
+
650
+ billingInfo = fieldSection?.billing
651
+ }
652
+
653
+ @IBAction func actionBtnSelectCountry(_ sender: UIButton) {
654
+ UIView.animate(withDuration: 0.3) {
655
+ self.viewCountryList.isHidden.toggle()
656
+ }
657
+ }
658
+
659
+ @IBAction func actionBtnSelectState(_ sender: UIButton) {
660
+ isSearchingState = false
661
+ filteredStateList = stateList // Reset list to full states
662
+ tblViewStateList.reloadData() // Reload table with all states
663
+
664
+ UIView.animate(withDuration: 0.3) {
665
+ self.viewStateList.isHidden.toggle()
666
+ }
667
+ }
668
+
669
+ @IBAction func actionBtnSelectCity(_ sender: UIButton) {
670
+ UIView.animate(withDuration: 0.3) {
671
+ self.viewCityList.isHidden.toggle()
672
+ }
673
+ }
674
+
675
+ @IBAction func actionBtnPrevious(_ sender: UIButton) {
676
+ // Pass the text back to the previous screen
677
+ delegate?.didPassTextBack("NewAccount")
678
+ self.navigationController?.popViewController(animated: true)
679
+ }
680
+
681
+ @IBAction func actionBtnNext(_ sender: UIButton) {
682
+ guard let request = request else { return }
683
+
684
+ // MARK: - Validate Billing Fields
685
+ if !lblStarAddressField.isHidden &&
686
+ txtFieldAddress.text?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == true {
687
+ showAlert(title: "Missing Information", message: "Please enter your address.")
688
+ return
689
+ } else if !lblStarCountryField.isHidden &&
690
+ txtFieldCountry.text?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == true {
691
+ showAlert(title: "Missing Information", message: "Please select your country.")
692
+ return
693
+ } else if !lblStarStateField.isHidden &&
694
+ txtFieldState.text?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == true {
695
+ showAlert(title: "Missing Information", message: "Please select your state.")
696
+ return
697
+ } else if !lblStarCityField.isHidden &&
698
+ txtFieldCity.text?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == true {
699
+ showAlert(title: "Missing Information", message: "Please select your city.")
700
+ return
701
+ } else if !lblStarPostalCodeField.isHidden &&
702
+ txtFieldPostalCode.text?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == true {
703
+ showAlert(title: "Missing Information", message: "Please enter your postal code.")
704
+ return
705
+ }
706
+
707
+ // Update billing info
708
+ updateBillingInfoData()
709
+
710
+ guard let updatedFieldSection = fieldSection else {
711
+ return
712
+ }
713
+
714
+ let isAdditionalVisible = updatedFieldSection.visibility.additional
715
+
716
+ // Get updated billingInfoData after updateBillingInfoData()
717
+ guard let updatedBillingData = try? JSONEncoder().encode(fieldSection),
718
+ let updatedFieldSection = try? JSONDecoder().decode(FieldSection.self, from: updatedBillingData) else {
719
+ return
720
+ }
721
+
722
+ // MARK: - Card Flow
723
+ if selectedPaymentMethod == "Card" {
724
+
725
+ if isAdditionalVisible {
726
+ // ✅ Go to AdditionalInfoVC
727
+ let vc = EasyPaySdk.instantiateViewController(withIdentifier: "AdditionalInfoVC") as! AdditionalInfoVC
728
+ vc.cardNumber = cardNumber
729
+ vc.expiryDate = expiryDate
730
+ vc.cvv = cvv
731
+ vc.nameOnCard = nameOnCard
732
+ vc.userEmail = userEmail
733
+ vc.billingInfoData = updatedBillingData
734
+ vc.fieldSection = updatedFieldSection
735
+ vc.billingInfo = updatedFieldSection.billing
736
+ vc.additionalInfo = updatedFieldSection.additional
737
+ vc.visibility = updatedFieldSection.visibility
738
+ vc.selectedPaymentMethod = selectedPaymentMethod
739
+ vc.isSavedForFuture = isSavedForFuture
740
+ vc.amount = Double(self.request.amount ?? 0)
741
+ vc.request = request
742
+ vc.chosenPlan = chosenPlan
743
+ vc.startDate = startDate
744
+ vc.isFrom = isFrom
745
+ if isFrom == "SavedCards" {
746
+ vc.selectedCard = selectedCard
747
+ vc.cvvText = cvvText
748
+ }
749
+ else if isFrom == "AddNewCard" {
750
+ vc.isSavedNewCard = isSavedNewCard
751
+ }
752
+ navigationController?.pushViewController(vc, animated: true)
753
+
754
+ }
755
+ else if !isAdditionalVisible && isSavedForFuture {
756
+ if UserStoreSingleton.shared.isLoggedIn == true {
757
+ if request.secureAuthentication == true {
758
+ threeDSecurePaymentApi()
759
+ } else {
760
+ paymentIntentApi()
761
+ }
762
+ } else {
763
+ // let vc = EasyPaySdk.instantiateViewController(withIdentifier: "EmailVerificationVC") as! EmailVerificationVC
764
+ let vc = EasyPaySdk.instantiateViewController(withIdentifier: "OTPVerificationVC") as! OTPVerificationVC
765
+ vc.cardNumber = cardNumber
766
+ vc.expiryDate = expiryDate
767
+ vc.cvv = cvv
768
+ vc.nameOnCard = nameOnCard
769
+ vc.userEmail = userEmail
770
+ vc.billingInfoData = updatedBillingData
771
+ vc.fieldSection = updatedFieldSection
772
+ vc.selectedPaymentMethod = selectedPaymentMethod
773
+ vc.easyPayDelegate = easyPayDelegate
774
+ vc.request = request
775
+ vc.chosenPlan = chosenPlan
776
+ vc.startDate = startDate
777
+ vc.billingInfo = updatedFieldSection.billing
778
+ vc.additionalInfo = updatedFieldSection.additional
779
+ vc.visibility = updatedFieldSection.visibility
780
+ vc.isSavedForFuture = true
781
+ vc.amount = amount
782
+ navigationController?.pushViewController(vc, animated: true)
783
+ }
784
+ }
785
+ else if !isAdditionalVisible && isFrom == "SavedCards" {
786
+ paymentIntentFromShowCardApi()
787
+ }
788
+ else if !isAdditionalVisible && isSavedNewCard {
789
+ if isFrom == "AddNewCard" {
790
+ if request.secureAuthentication == true {
791
+ threeDSecurePaymentAddNewCardApi(customerId: UserStoreSingleton.shared.customerId)
792
+ }
793
+ else {
794
+ paymentIntentAddNewCardApi(customerId: UserStoreSingleton.shared.customerId)
795
+ }
796
+ }
797
+ }
798
+ else {
799
+ // ✅ Skip to Payment directly
800
+ if request.secureAuthentication == true {
801
+ threeDSecurePaymentApi()
802
+ } else {
803
+ paymentIntentApi()
804
+ }
805
+ }
806
+ }
807
+ else if selectedPaymentMethod == "Bank" {
808
+ if isAdditionalVisible {
809
+ // ✅ Go to AdditionalInfoVC
810
+ let vc = EasyPaySdk.instantiateViewController(withIdentifier: "AdditionalInfoVC") as! AdditionalInfoVC
811
+ vc.userEmail = userEmail
812
+ vc.billingInfoData = updatedBillingData
813
+ vc.fieldSection = updatedFieldSection
814
+ vc.billingInfo = updatedFieldSection.billing
815
+ vc.additionalInfo = updatedFieldSection.additional
816
+ vc.visibility = updatedFieldSection.visibility
817
+ vc.selectedPaymentMethod = selectedPaymentMethod
818
+ vc.isSavedForFuture = isSavedForFuture
819
+ vc.accountName = accountName
820
+ vc.routingNumber = routingNumber
821
+ vc.accountType = accountType
822
+ vc.accountNumber = accountNumber
823
+ vc.customerID = customerID
824
+ vc.accountID = accountID
825
+ vc.isFrom = isFrom
826
+ vc.isSavedNewAccount = isSavedNewAccount
827
+ vc.amount = Double(self.request.amount ?? 0)
828
+ vc.request = request
829
+ vc.chosenPlan = chosenPlan
830
+ vc.startDate = startDate
831
+ vc.grailPayAccountID = grailPayAccountID
832
+ vc.selectedGrailPayAccountType = selectedGrailPayAccountType
833
+ vc.selectedGrailPayAccountName = selectedGrailPayAccountName
834
+ navigationController?.pushViewController(vc, animated: true)
835
+ }
836
+ else if !isAdditionalVisible && isSavedForFuture {
837
+ if UserStoreSingleton.shared.isLoggedIn == true {
838
+ accountChargeWithSaveApi(customerId: UserStoreSingleton.shared.customerId)
839
+ } else {
840
+ // let vc = EasyPaySdk.instantiateViewController(withIdentifier: "EmailVerificationVC") as! EmailVerificationVC
841
+ let vc = EasyPaySdk.instantiateViewController(withIdentifier: "OTPVerificationVC") as! OTPVerificationVC
842
+ vc.accountName = accountName
843
+ vc.routingNumber = routingNumber
844
+ vc.accountType = accountType
845
+ vc.accountNumber = accountNumber
846
+ vc.userEmail = userEmail
847
+ vc.billingInfoData = updatedBillingData
848
+ vc.fieldSection = updatedFieldSection
849
+ vc.selectedPaymentMethod = selectedPaymentMethod
850
+ vc.easyPayDelegate = easyPayDelegate
851
+ vc.request = self.request
852
+ vc.chosenPlan = chosenPlan
853
+ vc.startDate = startDate
854
+ vc.billingInfo = updatedFieldSection.billing
855
+ vc.additionalInfo = updatedFieldSection.additional
856
+ vc.visibility = updatedFieldSection.visibility
857
+ vc.isSavedForFuture = isSavedForFuture
858
+ vc.isSavedNewAccount = isSavedNewAccount
859
+ vc.amount = amount
860
+ vc.grailPayAccountID = grailPayAccountID
861
+ vc.selectedGrailPayAccountType = selectedGrailPayAccountType
862
+ vc.selectedGrailPayAccountName = selectedGrailPayAccountName
863
+ vc.email = userEmail
864
+ navigationController?.pushViewController(vc, animated: true)
865
+ }
866
+ }
867
+ else if isFrom == "SavedBank" {
868
+ accountChargeSavedBankAccountApi()
869
+ }
870
+ else if isFrom == "NormalBankPayWithoutSave" {
871
+ accountChargeApi()
872
+ }
873
+ else if isFrom == "AddNewAccountWithoutSave" {
874
+ accountChargeApi()
875
+ }
876
+ else if isFrom == "AddNewAccountWithSave" {
877
+ accountChargeApi(customerId: UserStoreSingleton.shared.customerId)
878
+ }
879
+ }
880
+ else if selectedPaymentMethod == "GrailPay" {
881
+ if isAdditionalVisible {
882
+ // ✅ Go to AdditionalInfoVC
883
+ let vc = EasyPaySdk.instantiateViewController(withIdentifier: "AdditionalInfoVC") as! AdditionalInfoVC
884
+ vc.billingInfoData = updatedBillingData
885
+ vc.fieldSection = updatedFieldSection
886
+ vc.billingInfo = updatedFieldSection.billing
887
+ vc.additionalInfo = updatedFieldSection.additional
888
+ vc.visibility = updatedFieldSection.visibility
889
+ vc.selectedPaymentMethod = selectedPaymentMethod
890
+ vc.isSavedForFuture = isSavedForFuture
891
+ vc.isFrom = isFrom
892
+ vc.isSavedNewAccount = isSavedNewAccount
893
+ vc.amount = Double(self.request.amount ?? 0)
894
+ vc.request = request
895
+ vc.chosenPlan = chosenPlan
896
+ vc.startDate = startDate
897
+ vc.grailPayAccountID = grailPayAccountID
898
+ vc.selectedGrailPayAccountType = selectedGrailPayAccountType
899
+ vc.selectedGrailPayAccountName = selectedGrailPayAccountName
900
+ vc.userEmail = userEmail
901
+ navigationController?.pushViewController(vc, animated: true)
902
+ }
903
+ else if !isAdditionalVisible && isSavedForFuture {
904
+ if UserStoreSingleton.shared.isLoggedIn == true {
905
+ grailPayAccountChargeApi()
906
+ } else {
907
+ // let vc = EasyPaySdk.instantiateViewController(withIdentifier: "EmailVerificationVC") as! EmailVerificationVC
908
+ let vc = EasyPaySdk.instantiateViewController(withIdentifier: "OTPVerificationVC") as! OTPVerificationVC
909
+ vc.billingInfoData = updatedBillingData
910
+ vc.fieldSection = updatedFieldSection
911
+ vc.billingInfo = updatedFieldSection.billing
912
+ vc.additionalInfo = updatedFieldSection.additional
913
+ vc.visibility = updatedFieldSection.visibility
914
+ vc.selectedPaymentMethod = selectedPaymentMethod
915
+ vc.isSavedForFuture = isSavedForFuture
916
+ vc.isFrom = isFrom
917
+ vc.isSavedNewAccount = isSavedNewAccount
918
+ vc.amount = Double(self.request.amount ?? 0)
919
+ vc.request = request
920
+ vc.chosenPlan = chosenPlan
921
+ vc.startDate = startDate
922
+ vc.grailPayAccountID = grailPayAccountID
923
+ vc.selectedGrailPayAccountType = selectedGrailPayAccountType
924
+ vc.selectedGrailPayAccountName = selectedGrailPayAccountName
925
+ vc.userEmail = userEmail
926
+ vc.email = userEmail
927
+
928
+ vc.grailPayAccountID = self.grailPayAccountID
929
+ vc.selectedGrailPayAccountType = self.selectedGrailPayAccountType
930
+ vc.selectedGrailPayAccountName = self.selectedGrailPayAccountName
931
+ navigationController?.pushViewController(vc, animated: true)
932
+ }
933
+ }
934
+ else {
935
+ grailPayAccountChargeApi()
936
+ }
937
+ }
938
+ else if selectedPaymentMethod == "NewGrailPayAccount" {
939
+ if isAdditionalVisible {
940
+ // ✅ Go to AdditionalInfoVC
941
+ let vc = EasyPaySdk.instantiateViewController(withIdentifier: "AdditionalInfoVC") as! AdditionalInfoVC
942
+ vc.billingInfoData = updatedBillingData
943
+ vc.fieldSection = updatedFieldSection
944
+ vc.billingInfo = updatedFieldSection.billing
945
+ vc.additionalInfo = updatedFieldSection.additional
946
+ vc.visibility = updatedFieldSection.visibility
947
+ vc.selectedPaymentMethod = selectedPaymentMethod
948
+ vc.isSavedForFuture = isSavedForFuture
949
+ vc.isFrom = isFrom
950
+ vc.isSavedNewAccount = isSavedNewAccount
951
+ vc.amount = Double(self.request.amount ?? 0)
952
+ vc.request = request
953
+ vc.chosenPlan = chosenPlan
954
+ vc.startDate = startDate
955
+ vc.grailPayAccountID = grailPayAccountID
956
+ vc.selectedGrailPayAccountType = selectedGrailPayAccountType
957
+ vc.selectedGrailPayAccountName = selectedGrailPayAccountName
958
+ navigationController?.pushViewController(vc, animated: true)
959
+ }
960
+ else if !isAdditionalVisible && isSavedForFuture {
961
+ grailPayAccountChargeApi(customerId: UserStoreSingleton.shared.customerId)
962
+ }
963
+ else {
964
+ grailPayAccountChargeApi()
965
+ }
966
+ }
967
+ }
968
+
969
+ // MARK: - Credit Card Charge Api
970
+ func paymentIntentApi() {
971
+ showLoadingIndicator()
972
+
973
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.charges.path()
974
+
975
+ guard let serviceURL = URL(string: fullURL) else {
976
+ hideLoadingIndicator()
977
+ return
978
+ }
979
+
980
+ var urlRequest = URLRequest(url: serviceURL)
981
+ urlRequest.httpMethod = "POST"
982
+ urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
983
+
984
+ let token = UserStoreSingleton.shared.clientToken
985
+ urlRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
986
+
987
+ var params: [String: Any] = [
988
+ "name": nameOnCard ?? "",
989
+ "email": userEmail ?? "",
990
+ "card_number": cardNumber?.replacingOccurrences(of: " ", with: "") ?? "",
991
+ "cardholder_name": nameOnCard ?? "",
992
+ "exp_month": expiryDate?.components(separatedBy: "/").first ?? "",
993
+ "exp_year": expiryDate?.components(separatedBy: "/").last ?? "",
994
+ "cvc": cvv ?? "",
995
+ "currency": "usd",
996
+ "description": "Hosted payment checkout"
997
+ ]
998
+
999
+ // Conditionally add billing info
1000
+ if let visibility = visibility, visibility.billing == true,
1001
+ let billing = billingInfo, !billing.isEmpty {
1002
+
1003
+ var billingInfoDict: [String: Any] = [:]
1004
+ for item in billing {
1005
+ billingInfoDict[item.name] = item.value
1006
+ }
1007
+
1008
+ params["address"] = billingInfoDict["address"] as? String ?? ""
1009
+ params["country"] = billingInfoDict["country"] as? String ?? ""
1010
+ params["state"] = billingInfoDict["state"] as? String ?? ""
1011
+ params["city"] = billingInfoDict["city"] as? String ?? ""
1012
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
1013
+ }
1014
+
1015
+ // Add these if recurring is enabled
1016
+ // if let req = request, req.is_recurring == true {
1017
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1018
+ // // Only send start_date if type is .custom and field is not empty
1019
+ // if let startDateText = startDate, !startDateText.isEmpty {
1020
+ // let inputFormatter = DateFormatter()
1021
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1022
+ //
1023
+ // let outputFormatter = DateFormatter()
1024
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
1025
+ //
1026
+ // if let date = inputFormatter.date(from: startDateText) {
1027
+ // let apiFormattedDate = outputFormatter.string(from: date)
1028
+ // params["start_date"] = apiFormattedDate
1029
+ // } else {
1030
+ // }
1031
+ // }
1032
+ // }
1033
+ //
1034
+ // params["interval"] = chosenPlan?.lowercased()
1035
+ // }
1036
+
1037
+ // Add these if recurring is enabled
1038
+ if let req = request, req.is_recurring == true {
1039
+ if let startDateText = startDate, !startDateText.isEmpty {
1040
+ let inputFormatter = DateFormatter()
1041
+ inputFormatter.dateFormat = "dd/MM/yyyy"
1042
+
1043
+ let outputFormatter = DateFormatter()
1044
+ outputFormatter.dateFormat = "MM/dd/yyyy"
1045
+
1046
+ if let date = inputFormatter.date(from: startDateText) {
1047
+ let apiFormattedDate = outputFormatter.string(from: date)
1048
+ params["start_date"] = apiFormattedDate
1049
+ } else {
1050
+ }
1051
+ }
1052
+
1053
+ // interval is still required
1054
+ params["interval"] = chosenPlan?.lowercased()
1055
+ }
1056
+
1057
+ // ✅ Include metadata only if it has at least 1 key-value pair
1058
+ if let metadata = request?.metadata, !metadata.isEmpty {
1059
+ params["metadata"] = metadata
1060
+ }
1061
+
1062
+ // ✅ Include metadata only if it has at least 1 key-value pair
1063
+ if let metadata = request?.metadata, !metadata.isEmpty {
1064
+ params["metadata"] = metadata
1065
+ }
1066
+
1067
+ // ✅ Only for logged-in users
1068
+ if UserStoreSingleton.shared.isLoggedIn == true {
1069
+ let emailText = userEmail
1070
+ let emailPrefix = emailText?.components(separatedBy: "@").first ?? ""
1071
+
1072
+ params["save_card"] = isSavedNewCard ? 1 : 0
1073
+ if isSavedNewCard {
1074
+ params["is_default"] = "1"
1075
+ }
1076
+ params["tokenize"] = request.tokenOnly ?? ""
1077
+ params["username"] = emailPrefix
1078
+
1079
+ if let customerId = UserStoreSingleton.shared.customerId {
1080
+ params["customer"] = customerId
1081
+ params["customer_id"] = customerId
1082
+ } else {
1083
+ params["create_customer"] = "1"
1084
+ }
1085
+
1086
+ if UserStoreSingleton.shared.customerId == nil {
1087
+ params["create_customer"] = "1"
1088
+ }
1089
+ }
1090
+
1091
+
1092
+ do {
1093
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
1094
+ urlRequest.httpBody = jsonData
1095
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1096
+ }
1097
+ } catch let error {
1098
+ hideLoadingIndicator()
1099
+ return
1100
+ }
1101
+
1102
+ let session = URLSession.shared
1103
+ let task = session.dataTask(with: urlRequest) { (serviceData, serviceResponse, error) in
1104
+
1105
+ DispatchQueue.main.async {
1106
+ self.hideLoadingIndicator()
1107
+ }
1108
+
1109
+ if let error = error {
1110
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
1111
+ return
1112
+ }
1113
+
1114
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
1115
+ self.presentPaymentErrorVC(errorMessage: "Invalid response from server.")
1116
+ return
1117
+ }
1118
+
1119
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
1120
+ if let data = serviceData {
1121
+ do {
1122
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
1123
+ // ✅ Handle duplicate transaction case
1124
+ if let status = responseObject["status"] as? Bool, status == false,
1125
+ let message = responseObject["message"] as? String,
1126
+ message.lowercased().contains("duplicate transaction") {
1127
+ self.presentPaymentErrorVC(errorMessage: message)
1128
+ return
1129
+ }
1130
+
1131
+ if let status = responseObject["status"] as? Int, status == 0,
1132
+ let message = responseObject["message"] as? String,
1133
+ message.lowercased().contains("duplicate transaction") {
1134
+ self.presentPaymentErrorVC(errorMessage: message)
1135
+ return
1136
+ }
1137
+
1138
+ // ✅ Handle generic "status == 0" error case
1139
+ if let status = responseObject["status"] as? Int, status == 0 {
1140
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
1141
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
1142
+ return
1143
+ }
1144
+ else {
1145
+ DispatchQueue.main.async {
1146
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
1147
+ paymentDoneVC.chargeData = responseObject
1148
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
1149
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
1150
+ paymentDoneVC.visibility = self.visibility
1151
+ paymentDoneVC.request = self.request
1152
+
1153
+ // if self.visibility?.billing == true {
1154
+ paymentDoneVC.billingInfoData = self.billingInfo
1155
+ var billingDict: [String: Any] = [:]
1156
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
1157
+ paymentDoneVC.billingInfo = billingDict
1158
+ // }
1159
+
1160
+ // if self.visibility?.additional == true {
1161
+ paymentDoneVC.additionalInfoData = self.additionalInfo
1162
+ var additionalDict: [String: Any] = [:]
1163
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
1164
+ paymentDoneVC.additionalInfo = additionalDict
1165
+ // }
1166
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
1167
+ }
1168
+ }
1169
+ }
1170
+ } else {
1171
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
1172
+ }
1173
+ } catch let jsonError {
1174
+ self.presentPaymentErrorVC(errorMessage: "Error parsing response: \(jsonError.localizedDescription)")
1175
+ }
1176
+ } else {
1177
+ self.presentPaymentErrorVC(errorMessage: "No data received from server.")
1178
+ }
1179
+ } else {
1180
+ if let data = serviceData,
1181
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
1182
+ let message = responseObj["message"] as? String {
1183
+ self.presentPaymentErrorVC(errorMessage: message)
1184
+ } else {
1185
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
1186
+ }
1187
+ }
1188
+ }
1189
+ task.resume()
1190
+ }
1191
+
1192
+ func presentPaymentErrorVC(errorMessage: String) {
1193
+ DispatchQueue.main.async {
1194
+ if let paymentErrorVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentErrorVC") as? PaymentErrorVC {
1195
+ paymentErrorVC.errorMessage = errorMessage
1196
+ paymentErrorVC.easyPayDelegate = self.easyPayDelegate // Pass the reference here
1197
+ self.navigationController?.pushViewController(paymentErrorVC, animated: true)
1198
+ }
1199
+ }
1200
+ }
1201
+
1202
+ //MARK: - 3DSecure
1203
+ // MARK: - Credit Card Charge Api If Billing info is not nil and Without Login.
1204
+ func threeDSecurePaymentApi() {
1205
+ showLoadingIndicator()
1206
+
1207
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.threeDSecure.path()
1208
+
1209
+ guard let serviceURL = URL(string: fullURL) else {
1210
+ hideLoadingIndicator()
1211
+ return
1212
+ }
1213
+
1214
+ var uRLRequest = URLRequest(url: serviceURL)
1215
+ uRLRequest.httpMethod = "POST"
1216
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
1217
+
1218
+ let token = UserStoreSingleton.shared.clientToken
1219
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
1220
+
1221
+ var params: [String: Any] = [
1222
+ "name": nameOnCard ?? "",
1223
+ "email": userEmail ?? "",
1224
+ "card_number": cardNumber?.replacingOccurrences(of: " ", with: "") ?? "",
1225
+ "cardholder_name": nameOnCard ?? "",
1226
+ "exp_month": expiryDate?.components(separatedBy: "/").first ?? "",
1227
+ "exp_year": expiryDate?.components(separatedBy: "/").last ?? "",
1228
+ "cvc": cvv ?? "",
1229
+ "currency": "usd",
1230
+ "tokenize": request.tokenOnly ?? false
1231
+ ]
1232
+
1233
+ // ✅ Only for logged-in users
1234
+ if UserStoreSingleton.shared.isLoggedIn == true {
1235
+ let emailText = userEmail
1236
+ let emailPrefix = emailText?.components(separatedBy: "@").first ?? ""
1237
+
1238
+ params["save_card"] = isSavedNewCard ? 1 : 0
1239
+ if isSavedNewCard {
1240
+ params["is_default"] = "1"
1241
+ }
1242
+ params["tokenize"] = request.tokenOnly ?? ""
1243
+ params["username"] = emailPrefix
1244
+
1245
+ if let customerId = UserStoreSingleton.shared.customerId {
1246
+ params["customer"] = customerId
1247
+ params["customer_id"] = customerId
1248
+ } else {
1249
+ params["create_customer"] = "1"
1250
+ }
1251
+
1252
+ if UserStoreSingleton.shared.customerId == nil {
1253
+ params["create_customer"] = "1"
1254
+ }
1255
+ }
1256
+
1257
+ // Conditionally add billing info
1258
+ if let visibility = visibility, visibility.billing == true,
1259
+ let billing = billingInfo, !billing.isEmpty {
1260
+
1261
+ var billingInfoDict: [String: Any] = [:]
1262
+ for item in billing {
1263
+ billingInfoDict[item.name] = item.value
1264
+ }
1265
+
1266
+ params["address"] = billingInfoDict["address"] as? String ?? ""
1267
+ params["country"] = billingInfoDict["country"] as? String ?? ""
1268
+ params["state"] = billingInfoDict["state"] as? String ?? ""
1269
+ params["city"] = billingInfoDict["city"] as? String ?? ""
1270
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
1271
+ }
1272
+
1273
+ // Set default description if additional info is not visible
1274
+ if let visibility = visibility, visibility.additional == false {
1275
+ params["description"] = "Hosted payment checkout"
1276
+ }
1277
+
1278
+ // Add these if recurring is enabled
1279
+ // if let req = request, req.is_recurring == true {
1280
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1281
+ // // Only send start_date if type is .custom and field is not empty
1282
+ // if let startDateText = startDate, !startDateText.isEmpty {
1283
+ // let inputFormatter = DateFormatter()
1284
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1285
+ //
1286
+ // let outputFormatter = DateFormatter()
1287
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
1288
+ //
1289
+ // if let date = inputFormatter.date(from: startDateText) {
1290
+ // let apiFormattedDate = outputFormatter.string(from: date)
1291
+ // params["start_date"] = apiFormattedDate
1292
+ // } else {
1293
+ // }
1294
+ // }
1295
+ // }
1296
+ //
1297
+ // params["interval"] = chosenPlan?.lowercased()
1298
+ // }
1299
+
1300
+ // Add these if recurring is enabled
1301
+ if let req = request, req.is_recurring == true {
1302
+ if let startDateText = startDate, !startDateText.isEmpty {
1303
+ let inputFormatter = DateFormatter()
1304
+ inputFormatter.dateFormat = "dd/MM/yyyy"
1305
+
1306
+ let outputFormatter = DateFormatter()
1307
+ outputFormatter.dateFormat = "MM/dd/yyyy"
1308
+
1309
+ if let date = inputFormatter.date(from: startDateText) {
1310
+ let apiFormattedDate = outputFormatter.string(from: date)
1311
+ params["start_date"] = apiFormattedDate
1312
+ } else {
1313
+ }
1314
+ }
1315
+
1316
+ // interval is still required
1317
+ params["interval"] = chosenPlan?.lowercased()
1318
+ }
1319
+
1320
+ // ✅ Include metadata only if it has at least 1 key-value pair
1321
+ if let metadata = request?.metadata, !metadata.isEmpty {
1322
+ params["metadata"] = metadata
1323
+ }
1324
+
1325
+
1326
+ do {
1327
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
1328
+ uRLRequest.httpBody = jsonData
1329
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1330
+ }
1331
+ } catch let error {
1332
+ hideLoadingIndicator()
1333
+ return
1334
+ }
1335
+
1336
+ let session = URLSession.shared
1337
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
1338
+
1339
+ DispatchQueue.main.async {
1340
+ self.hideLoadingIndicator() // Stop loader when response is received
1341
+ }
1342
+
1343
+ if let error = error {
1344
+ return
1345
+ }
1346
+
1347
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
1348
+ return
1349
+ }
1350
+
1351
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
1352
+ if let data = serviceData {
1353
+ do {
1354
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
1355
+
1356
+ // ✅ Handle duplicate transaction case
1357
+ if let status = responseObject["status"] as? Bool, status == false,
1358
+ let message = responseObject["message"] as? String,
1359
+ message.lowercased().contains("duplicate transaction") {
1360
+ self.presentPaymentErrorVC(errorMessage: message)
1361
+ return
1362
+ }
1363
+
1364
+ if let status = responseObject["status"] as? Int, status == 0,
1365
+ let message = responseObject["message"] as? String,
1366
+ message.lowercased().contains("duplicate transaction") {
1367
+ self.presentPaymentErrorVC(errorMessage: message)
1368
+ return
1369
+ }
1370
+
1371
+ // ✅ Handle generic "status == 0" error case
1372
+ if let status = responseObject["status"] as? Int, status == 0 {
1373
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
1374
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
1375
+ return
1376
+ }
1377
+ else {
1378
+ DispatchQueue.main.async {
1379
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "ThreeDSecurePaymentDoneVC") as? ThreeDSecurePaymentDoneVC {
1380
+
1381
+ let urlString = responseObject["redirect_url"] as? String ?? responseObject["location_url"] as? String ?? ""
1382
+ paymentDoneVC.redirectURL = urlString
1383
+ paymentDoneVC.chargeData = responseObject
1384
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
1385
+ // Pass billing and additional info
1386
+ // Conditionally pass raw FieldItem array
1387
+ paymentDoneVC.visibility = self.visibility
1388
+ paymentDoneVC.amount = self.amount
1389
+ paymentDoneVC.cardApiParams = params
1390
+ paymentDoneVC.request = self.request
1391
+
1392
+ // if self.visibility?.billing == true {
1393
+ paymentDoneVC.billingInfoData = self.billingInfo
1394
+ var billingDict: [String: Any] = [:]
1395
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
1396
+ paymentDoneVC.billingInfo = billingDict
1397
+ // }
1398
+
1399
+ // if self.visibility?.additional == true {
1400
+ paymentDoneVC.additionalInfoData = self.additionalInfo
1401
+ var additionalDict: [String: Any] = [:]
1402
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
1403
+ paymentDoneVC.additionalInfo = additionalDict
1404
+ // }
1405
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
1406
+ }
1407
+ }
1408
+ }
1409
+ } else {
1410
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
1411
+ }
1412
+ } catch let jsonError {
1413
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
1414
+ }
1415
+ } else {
1416
+ self.presentPaymentErrorVC(errorMessage: "No data received")
1417
+ }
1418
+ } else {
1419
+ if let data = serviceData,
1420
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
1421
+ let message = responseObj["message"] as? String {
1422
+ self.presentPaymentErrorVC(errorMessage: message)
1423
+ } else {
1424
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
1425
+ }
1426
+ }
1427
+ }
1428
+ task.resume()
1429
+ }
1430
+
1431
+ // MARK: - Credit Card Charge Api If Billing info is not nil With Login from Add New Card.
1432
+ func threeDSecurePaymentAddNewCardApi(customerId: String?) {
1433
+ showLoadingIndicator()
1434
+
1435
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.threeDSecure.path()
1436
+
1437
+ guard let serviceURL = URL(string: fullURL) else {
1438
+ hideLoadingIndicator()
1439
+ return
1440
+ }
1441
+
1442
+ var uRLRequest = URLRequest(url: serviceURL)
1443
+ uRLRequest.httpMethod = "POST"
1444
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
1445
+
1446
+ let token = UserStoreSingleton.shared.clientToken
1447
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
1448
+
1449
+ var params: [String: Any] = [
1450
+ "name": nameOnCard ?? "",
1451
+ "email": userEmail ?? "",
1452
+ "card_number": cardNumber?.replacingOccurrences(of: " ", with: "") ?? "",
1453
+ "cardholder_name": nameOnCard ?? "",
1454
+ "exp_month": expiryDate?.components(separatedBy: "/").first ?? "",
1455
+ "exp_year": expiryDate?.components(separatedBy: "/").last ?? "",
1456
+ "cvc": cvv ?? "",
1457
+ "description": "Hosted payment checkout",
1458
+ "currency": "usd",
1459
+ "tokenize": request.tokenOnly ?? false,
1460
+ "save_card": isSavedNewCard ? 1 : 0,
1461
+ "customer_id": customerId ?? ""
1462
+ ]
1463
+
1464
+ // Add is_default parameter if save_card is 1
1465
+ if isSavedNewCard {
1466
+ params["is_default"] = "1"
1467
+ }
1468
+
1469
+ // Conditionally add billing info
1470
+ if let visibility = visibility, visibility.billing == true,
1471
+ let billing = billingInfo, !billing.isEmpty {
1472
+
1473
+ var billingInfoDict: [String: Any] = [:]
1474
+ for item in billing {
1475
+ billingInfoDict[item.name] = item.value
1476
+ }
1477
+
1478
+ params["address"] = billingInfoDict["address"] as? String ?? ""
1479
+ params["country"] = billingInfoDict["country"] as? String ?? ""
1480
+ params["state"] = billingInfoDict["state"] as? String ?? ""
1481
+ params["city"] = billingInfoDict["city"] as? String ?? ""
1482
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
1483
+ }
1484
+
1485
+ // Add these if recurring is enabled
1486
+ // if let req = request, req.is_recurring == true {
1487
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1488
+ // // Only send start_date if type is .custom and field is not empty
1489
+ // if let startDateText = startDate, !startDateText.isEmpty {
1490
+ // let inputFormatter = DateFormatter()
1491
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1492
+ //
1493
+ // let outputFormatter = DateFormatter()
1494
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
1495
+ //
1496
+ // if let date = inputFormatter.date(from: startDateText) {
1497
+ // let apiFormattedDate = outputFormatter.string(from: date)
1498
+ // params["start_date"] = apiFormattedDate
1499
+ // } else {
1500
+ // }
1501
+ // }
1502
+ // }
1503
+ //
1504
+ // params["interval"] = chosenPlan?.lowercased()
1505
+ // }
1506
+
1507
+ // Add these if recurring is enabled
1508
+ if let req = request, req.is_recurring == true {
1509
+ if let startDateText = startDate, !startDateText.isEmpty {
1510
+ let inputFormatter = DateFormatter()
1511
+ inputFormatter.dateFormat = "dd/MM/yyyy"
1512
+
1513
+ let outputFormatter = DateFormatter()
1514
+ outputFormatter.dateFormat = "MM/dd/yyyy"
1515
+
1516
+ if let date = inputFormatter.date(from: startDateText) {
1517
+ let apiFormattedDate = outputFormatter.string(from: date)
1518
+ params["start_date"] = apiFormattedDate
1519
+ } else {
1520
+ }
1521
+ }
1522
+
1523
+ // interval is still required
1524
+ params["interval"] = chosenPlan?.lowercased()
1525
+ }
1526
+
1527
+ // ✅ Include metadata only if it has at least 1 key-value pair
1528
+ if let metadata = request?.metadata, !metadata.isEmpty {
1529
+ params["metadata"] = metadata
1530
+ }
1531
+
1532
+ do {
1533
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
1534
+ uRLRequest.httpBody = jsonData
1535
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1536
+ }
1537
+ } catch let error {
1538
+ hideLoadingIndicator()
1539
+ return
1540
+ }
1541
+
1542
+ let session = URLSession.shared
1543
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
1544
+
1545
+ DispatchQueue.main.async {
1546
+ self.hideLoadingIndicator() // Stop loader when response is received
1547
+ }
1548
+
1549
+ if let error = error {
1550
+ return
1551
+ }
1552
+
1553
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
1554
+ return
1555
+ }
1556
+
1557
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
1558
+ if let data = serviceData {
1559
+ do {
1560
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
1561
+
1562
+ // ✅ Handle duplicate transaction case
1563
+ if let status = responseObject["status"] as? Bool, status == false,
1564
+ let message = responseObject["message"] as? String,
1565
+ message.lowercased().contains("duplicate transaction") {
1566
+ self.presentPaymentErrorVC(errorMessage: message)
1567
+ return
1568
+ }
1569
+
1570
+ if let status = responseObject["status"] as? Int, status == 0,
1571
+ let message = responseObject["message"] as? String,
1572
+ message.lowercased().contains("duplicate transaction") {
1573
+ self.presentPaymentErrorVC(errorMessage: message)
1574
+ return
1575
+ }
1576
+
1577
+ // ✅ Handle generic "status == 0" error case
1578
+ if let status = responseObject["status"] as? Int, status == 0 {
1579
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
1580
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
1581
+ return
1582
+ }
1583
+ else {
1584
+ DispatchQueue.main.async {
1585
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "ThreeDSecurePaymentDoneVC") as? ThreeDSecurePaymentDoneVC {
1586
+
1587
+ let urlString = responseObject["redirect_url"] as? String ?? responseObject["location_url"] as? String ?? ""
1588
+ paymentDoneVC.redirectURL = urlString
1589
+ paymentDoneVC.chargeData = responseObject
1590
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
1591
+ // Pass billing and additional info
1592
+ // Conditionally pass raw FieldItem array
1593
+ paymentDoneVC.visibility = self.visibility
1594
+ paymentDoneVC.amount = self.amount
1595
+ paymentDoneVC.cardApiParams = params
1596
+ paymentDoneVC.request = self.request
1597
+
1598
+ // if self.visibility?.billing == true {
1599
+ paymentDoneVC.billingInfoData = self.billingInfo
1600
+ var billingDict: [String: Any] = [:]
1601
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
1602
+ paymentDoneVC.billingInfo = billingDict
1603
+ // }
1604
+
1605
+ // if self.visibility?.additional == true {
1606
+ paymentDoneVC.additionalInfoData = self.additionalInfo
1607
+ var additionalDict: [String: Any] = [:]
1608
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
1609
+ paymentDoneVC.additionalInfo = additionalDict
1610
+ // }
1611
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
1612
+ }
1613
+ }
1614
+ }
1615
+ } else {
1616
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
1617
+ }
1618
+ } catch let jsonError {
1619
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
1620
+ }
1621
+ } else {
1622
+ self.presentPaymentErrorVC(errorMessage: "No data received")
1623
+ }
1624
+ } else {
1625
+ if let data = serviceData,
1626
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
1627
+ let message = responseObj["message"] as? String {
1628
+ self.presentPaymentErrorVC(errorMessage: message)
1629
+ } else {
1630
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
1631
+ }
1632
+ }
1633
+ }
1634
+ task.resume()
1635
+ }
1636
+
1637
+ //MARK: - Credit Card Charge Api from Add new card from saved cards.
1638
+ func paymentIntentAddNewCardApi(customerId: String?) {
1639
+ showLoadingIndicator()
1640
+
1641
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.charges.path()
1642
+
1643
+ guard let serviceURL = URL(string: fullURL) else {
1644
+ hideLoadingIndicator()
1645
+ return
1646
+ }
1647
+
1648
+ var uRLRequest = URLRequest(url: serviceURL)
1649
+ uRLRequest.httpMethod = "POST"
1650
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
1651
+
1652
+ let token = UserStoreSingleton.shared.clientToken
1653
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
1654
+
1655
+ // let emailPrefix = UserStoreSingleton.shared.verificationEmail?.components(separatedBy: "@").first ?? ""
1656
+
1657
+ let finalEmail: String
1658
+ if let verificationEmail = UserStoreSingleton.shared.verificationEmail, !verificationEmail.isEmpty {
1659
+ finalEmail = verificationEmail
1660
+ } else {
1661
+ finalEmail = request.email ?? ""
1662
+ }
1663
+
1664
+ let emailPrefix: String
1665
+ if !finalEmail.isEmpty {
1666
+ emailPrefix = finalEmail.components(separatedBy: "@").first ?? ""
1667
+ } else {
1668
+ emailPrefix = ""
1669
+ }
1670
+
1671
+ var params: [String: Any] = [
1672
+ "name": nameOnCard ?? "",
1673
+ "email": userEmail ?? "",
1674
+ "card_number": cardNumber?.replacingOccurrences(of: " ", with: "") ?? "",
1675
+ "cardholder_name": nameOnCard ?? "",
1676
+ "exp_month": expiryDate?.components(separatedBy: "/").first ?? "",
1677
+ "exp_year": expiryDate?.components(separatedBy: "/").last ?? "",
1678
+ "cvc": cvv ?? "",
1679
+ "currency": "usd",
1680
+ "payment_method": selectedPaymentMethod ?? "",
1681
+ "save_card": isSavedNewCard ? 1 : 0
1682
+ ]
1683
+
1684
+ // Add is_default parameter if save_card is 1
1685
+ if isSavedNewCard {
1686
+ params["is_default"] = "1"
1687
+ }
1688
+
1689
+ // Conditionally add billing info
1690
+ if let visibility = visibility, visibility.billing == true,
1691
+ let billing = billingInfo, !billing.isEmpty {
1692
+
1693
+ var billingInfoDict: [String: Any] = [:]
1694
+ for item in billing {
1695
+ billingInfoDict[item.name] = item.value
1696
+ }
1697
+
1698
+ params["address"] = billingInfoDict["address"] as? String ?? ""
1699
+ params["country"] = billingInfoDict["country"] as? String ?? ""
1700
+ params["state"] = billingInfoDict["state"] as? String ?? ""
1701
+ params["city"] = billingInfoDict["city"] as? String ?? ""
1702
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
1703
+ }
1704
+
1705
+ // Set default description if additional info is not visible
1706
+ if let visibility = visibility, visibility.additional == false {
1707
+ params["description"] = "Hosted payment checkout"
1708
+ }
1709
+
1710
+ // Add these if recurring is enabled
1711
+ // if let req = request, req.is_recurring == true {
1712
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1713
+ // // Only send start_date if type is .custom and field is not empty
1714
+ // if let startDateText = startDate, !startDateText.isEmpty {
1715
+ // let inputFormatter = DateFormatter()
1716
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1717
+ //
1718
+ // let outputFormatter = DateFormatter()
1719
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
1720
+ //
1721
+ // if let date = inputFormatter.date(from: startDateText) {
1722
+ // let apiFormattedDate = outputFormatter.string(from: date)
1723
+ // params["start_date"] = apiFormattedDate
1724
+ // } else {
1725
+ // }
1726
+ // }
1727
+ // }
1728
+ //
1729
+ // params["interval"] = chosenPlan?.lowercased()
1730
+ // }
1731
+
1732
+ // Add these if recurring is enabled
1733
+ if let req = request, req.is_recurring == true {
1734
+ if let startDateText = startDate, !startDateText.isEmpty {
1735
+ let inputFormatter = DateFormatter()
1736
+ inputFormatter.dateFormat = "dd/MM/yyyy"
1737
+
1738
+ let outputFormatter = DateFormatter()
1739
+ outputFormatter.dateFormat = "MM/dd/yyyy"
1740
+
1741
+ if let date = inputFormatter.date(from: startDateText) {
1742
+ let apiFormattedDate = outputFormatter.string(from: date)
1743
+ params["start_date"] = apiFormattedDate
1744
+ } else {
1745
+ }
1746
+ }
1747
+
1748
+ // interval is still required
1749
+ params["interval"] = chosenPlan?.lowercased()
1750
+ }
1751
+
1752
+ if let customerId = customerId {
1753
+ params["customer"] = customerId
1754
+ params["customer_id"] = customerId
1755
+ } else {
1756
+ params["username"] = emailPrefix
1757
+ params["email"] = finalEmail
1758
+ }
1759
+
1760
+ // ✅ Include metadata only if it has at least 1 key-value pair
1761
+ if let metadata = request?.metadata, !metadata.isEmpty {
1762
+ params["metadata"] = metadata
1763
+ }
1764
+
1765
+
1766
+ do {
1767
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
1768
+ uRLRequest.httpBody = jsonData
1769
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1770
+ }
1771
+ } catch let error {
1772
+ hideLoadingIndicator()
1773
+ return
1774
+ }
1775
+
1776
+ let session = URLSession.shared
1777
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
1778
+
1779
+ DispatchQueue.main.async {
1780
+ self.hideLoadingIndicator() // Stop loader when response is received
1781
+ }
1782
+
1783
+ if let error = error {
1784
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
1785
+ return
1786
+ }
1787
+
1788
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
1789
+ self.presentPaymentErrorVC(errorMessage: "Invalid response")
1790
+ return
1791
+ }
1792
+
1793
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
1794
+ if let data = serviceData {
1795
+ do {
1796
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
1797
+
1798
+ // ✅ Handle duplicate transaction case
1799
+ if let status = responseObject["status"] as? Bool, status == false,
1800
+ let message = responseObject["message"] as? String,
1801
+ message.lowercased().contains("duplicate transaction") {
1802
+ self.presentPaymentErrorVC(errorMessage: message)
1803
+ return
1804
+ }
1805
+
1806
+ if let status = responseObject["status"] as? Int, status == 0,
1807
+ let message = responseObject["message"] as? String,
1808
+ message.lowercased().contains("duplicate transaction") {
1809
+ self.presentPaymentErrorVC(errorMessage: message)
1810
+ return
1811
+ }
1812
+
1813
+ // ✅ Handle generic "status == 0" error case
1814
+ if let status = responseObject["status"] as? Int, status == 0 {
1815
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
1816
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
1817
+ return
1818
+ }
1819
+ else {
1820
+ DispatchQueue.main.async {
1821
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
1822
+ paymentDoneVC.chargeData = responseObject
1823
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
1824
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
1825
+ // Pass billing and additional info
1826
+ // Conditionally pass raw FieldItem array
1827
+ paymentDoneVC.visibility = self.visibility
1828
+ paymentDoneVC.request = self.request
1829
+
1830
+ // if self.visibility?.billing == true {
1831
+ paymentDoneVC.billingInfoData = self.billingInfo
1832
+ var billingDict: [String: Any] = [:]
1833
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
1834
+ paymentDoneVC.billingInfo = billingDict
1835
+ // }
1836
+
1837
+ // if self.visibility?.additional == true {
1838
+ paymentDoneVC.additionalInfoData = self.additionalInfo
1839
+ var additionalDict: [String: Any] = [:]
1840
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
1841
+ paymentDoneVC.additionalInfo = additionalDict
1842
+ // }
1843
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
1844
+ }
1845
+ }
1846
+ }
1847
+ } else {
1848
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
1849
+ }
1850
+ } catch let jsonError {
1851
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
1852
+ }
1853
+ } else {
1854
+ self.presentPaymentErrorVC(errorMessage: "No data received")
1855
+ }
1856
+ } else {
1857
+ if let data = serviceData,
1858
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
1859
+ let message = responseObj["message"] as? String {
1860
+ self.presentPaymentErrorVC(errorMessage: message)
1861
+ } else {
1862
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
1863
+ }
1864
+ }
1865
+ }
1866
+ task.resume()
1867
+ }
1868
+
1869
+ //MARK: - Credit Card Charge Api from Saved cards
1870
+ func paymentIntentFromShowCardApi() {
1871
+ showLoadingIndicator()
1872
+
1873
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.charges.path()
1874
+
1875
+ guard let serviceURL = URL(string: fullURL) else {
1876
+ hideLoadingIndicator()
1877
+ return
1878
+ }
1879
+
1880
+ var uRLRequest = URLRequest(url: serviceURL)
1881
+ uRLRequest.httpMethod = "POST"
1882
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
1883
+
1884
+ let token = UserStoreSingleton.shared.clientToken
1885
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
1886
+
1887
+ // let emailText = UserStoreSingleton.shared.verificationEmail ?? ""
1888
+ // let emailPrefix = emailText.components(separatedBy: "@").first ?? ""
1889
+
1890
+ let finalEmail: String
1891
+ if let verificationEmail = UserStoreSingleton.shared.verificationEmail, !verificationEmail.isEmpty {
1892
+ finalEmail = verificationEmail
1893
+ } else {
1894
+ finalEmail = request.email ?? ""
1895
+ }
1896
+
1897
+ let emailPrefix: String
1898
+ if !finalEmail.isEmpty {
1899
+ emailPrefix = finalEmail.components(separatedBy: "@").first ?? ""
1900
+ } else {
1901
+ emailPrefix = ""
1902
+ }
1903
+
1904
+ // Determine name: use request.name if available, otherwise fallback to email prefix
1905
+ let nameParam: String
1906
+ if let requestName = request.name, !requestName.trimmingCharacters(in: .whitespaces).isEmpty {
1907
+ nameParam = requestName
1908
+ } else {
1909
+ nameParam = emailPrefix
1910
+ }
1911
+
1912
+ var params: [String: Any] = [
1913
+ "description": "Hosted payment checkout",
1914
+ "currency": "usd",
1915
+ "payment_method": "card",
1916
+ "save_card": 0,
1917
+ "customer" : selectedCard?.customerId ?? "",
1918
+ "customer_id" : selectedCard?.customerId ?? "",
1919
+ "card_id" : selectedCard?.cardId ?? "",
1920
+ "cvc" : cvvText ?? "",
1921
+ // "name": UserStoreSingleton.shared.merchantName ?? "",
1922
+ "name": nameParam,
1923
+ "email": finalEmail
1924
+ ]
1925
+
1926
+ // Conditionally add billing info
1927
+ if let visibility = visibility, visibility.billing == true,
1928
+ let billing = billingInfo, !billing.isEmpty {
1929
+
1930
+ var billingInfoDict: [String: Any] = [:]
1931
+ for item in billing {
1932
+ billingInfoDict[item.name] = item.value
1933
+ }
1934
+
1935
+ params["address"] = billingInfoDict["address"] as? String ?? ""
1936
+ params["country"] = billingInfoDict["country"] as? String ?? ""
1937
+ params["state"] = billingInfoDict["state"] as? String ?? ""
1938
+ params["city"] = billingInfoDict["city"] as? String ?? ""
1939
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
1940
+ }
1941
+
1942
+ // Conditionally add additional info
1943
+ if let visibility = visibility, visibility.additional == true,
1944
+ let additional = additionalInfo, !additional.isEmpty {
1945
+
1946
+ var additionalInfoDict: [String: Any] = [:]
1947
+ for item in additional {
1948
+ additionalInfoDict[item.name] = item.value
1949
+ }
1950
+
1951
+ params["description"] = additionalInfoDict["description"] as? String ?? ""
1952
+ params["phone_number"] = additionalInfoDict["phone_number"] as? String ?? ""
1953
+ params["name"] = additionalInfoDict["name"] as? String ?? ""
1954
+ params["email"] = additionalInfoDict["email"] as? String ?? ""
1955
+ }
1956
+
1957
+ // Set default description if additional info is not visible
1958
+ if let visibility = visibility, visibility.additional == false {
1959
+ params["description"] = "Hosted payment checkout"
1960
+ }
1961
+
1962
+ // Add these if recurring is enabled
1963
+ // if let req = request, req.is_recurring == true {
1964
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1965
+ // // Only send start_date if type is .custom and field is not empty
1966
+ // if let startDateText = startDate, !startDateText.isEmpty {
1967
+ // let inputFormatter = DateFormatter()
1968
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1969
+ //
1970
+ // let outputFormatter = DateFormatter()
1971
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
1972
+ //
1973
+ // if let date = inputFormatter.date(from: startDateText) {
1974
+ // let apiFormattedDate = outputFormatter.string(from: date)
1975
+ // params["start_date"] = apiFormattedDate
1976
+ // } else {
1977
+ // }
1978
+ // }
1979
+ // }
1980
+ //
1981
+ // params["interval"] = chosenPlan?.lowercased()
1982
+ // }
1983
+
1984
+ // Add these if recurring is enabled
1985
+ if let req = request, req.is_recurring == true {
1986
+ if let startDateText = startDate, !startDateText.isEmpty {
1987
+ let inputFormatter = DateFormatter()
1988
+ inputFormatter.dateFormat = "dd/MM/yyyy"
1989
+
1990
+ let outputFormatter = DateFormatter()
1991
+ outputFormatter.dateFormat = "MM/dd/yyyy"
1992
+
1993
+ if let date = inputFormatter.date(from: startDateText) {
1994
+ let apiFormattedDate = outputFormatter.string(from: date)
1995
+ params["start_date"] = apiFormattedDate
1996
+ } else {
1997
+ }
1998
+ }
1999
+
2000
+ // interval is still required
2001
+ params["interval"] = chosenPlan?.lowercased()
2002
+ }
2003
+
2004
+ // ✅ Include metadata only if it has at least 1 key-value pair
2005
+ if let metadata = request?.metadata, !metadata.isEmpty {
2006
+ params["metadata"] = metadata
2007
+ }
2008
+
2009
+
2010
+ do {
2011
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
2012
+ uRLRequest.httpBody = jsonData
2013
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
2014
+ }
2015
+ } catch let error {
2016
+ hideLoadingIndicator()
2017
+ return
2018
+ }
2019
+
2020
+ let session = URLSession.shared
2021
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
2022
+
2023
+ DispatchQueue.main.async {
2024
+ self.hideLoadingIndicator() // Stop loader when response is received
2025
+ }
2026
+
2027
+ if let error = error {
2028
+ return
2029
+ }
2030
+
2031
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
2032
+ return
2033
+ }
2034
+
2035
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
2036
+ if let data = serviceData {
2037
+ do {
2038
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
2039
+
2040
+ // ✅ Handle duplicate transaction case
2041
+ if let status = responseObject["status"] as? Bool, status == false,
2042
+ let message = responseObject["message"] as? String,
2043
+ message.lowercased().contains("duplicate transaction") {
2044
+ self.presentPaymentErrorVC(errorMessage: message)
2045
+ return
2046
+ }
2047
+
2048
+ if let status = responseObject["status"] as? Int, status == 0,
2049
+ let message = responseObject["message"] as? String,
2050
+ message.lowercased().contains("duplicate transaction") {
2051
+ self.presentPaymentErrorVC(errorMessage: message)
2052
+ return
2053
+ }
2054
+
2055
+ // ✅ Handle generic "status == 0" error case
2056
+ if let status = responseObject["status"] as? Int, status == 0 {
2057
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
2058
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
2059
+ return
2060
+ }
2061
+ else {
2062
+ DispatchQueue.main.async {
2063
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
2064
+ paymentDoneVC.chargeData = responseObject
2065
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
2066
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
2067
+ // Pass billing and additional info
2068
+ // Conditionally pass raw FieldItem array
2069
+ paymentDoneVC.visibility = self.visibility
2070
+ paymentDoneVC.request = self.request
2071
+
2072
+ // if self.visibility?.billing == true {
2073
+ paymentDoneVC.billingInfoData = self.billingInfo
2074
+ var billingDict: [String: Any] = [:]
2075
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
2076
+ paymentDoneVC.billingInfo = billingDict
2077
+ // }
2078
+
2079
+ // if self.visibility?.additional == true {
2080
+ paymentDoneVC.additionalInfoData = self.additionalInfo
2081
+ var additionalDict: [String: Any] = [:]
2082
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
2083
+ paymentDoneVC.additionalInfo = additionalDict
2084
+ // }
2085
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
2086
+ }
2087
+ }
2088
+ }
2089
+ } else {
2090
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
2091
+ }
2092
+ } catch let jsonError {
2093
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
2094
+ }
2095
+ } else {
2096
+ self.presentPaymentErrorVC(errorMessage: "No data received")
2097
+ }
2098
+ } else {
2099
+ if let data = serviceData,
2100
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
2101
+ let message = responseObj["message"] as? String {
2102
+ self.presentPaymentErrorVC(errorMessage: message)
2103
+ } else {
2104
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
2105
+ }
2106
+ }
2107
+ }
2108
+ task.resume()
2109
+ }
2110
+
2111
+ //MARK: - Banking Account Charge Api from Regular saved bank account
2112
+ func accountChargeSavedBankAccountApi() {
2113
+ showLoadingIndicator()
2114
+
2115
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.achCharge.path()
2116
+
2117
+ guard let serviceURL = URL(string: fullURL) else {
2118
+ hideLoadingIndicator()
2119
+ return
2120
+ }
2121
+
2122
+ var uRLRequest = URLRequest(url: serviceURL)
2123
+ uRLRequest.httpMethod = "POST"
2124
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
2125
+
2126
+ let token = UserStoreSingleton.shared.clientToken
2127
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
2128
+
2129
+ // let emailText = UserStoreSingleton.shared.verificationEmail ?? ""
2130
+ // let emailPrefix = emailText.components(separatedBy: "@").first ?? ""
2131
+
2132
+ let finalEmail: String
2133
+ if let verificationEmail = UserStoreSingleton.shared.verificationEmail, !verificationEmail.isEmpty {
2134
+ finalEmail = verificationEmail
2135
+ } else {
2136
+ finalEmail = request.email ?? ""
2137
+ }
2138
+
2139
+ let emailPrefix: String
2140
+ if !finalEmail.isEmpty {
2141
+ emailPrefix = finalEmail.components(separatedBy: "@").first ?? ""
2142
+ } else {
2143
+ emailPrefix = ""
2144
+ }
2145
+
2146
+ // Determine name: use request.name if available, otherwise fallback to email prefix
2147
+ let nameParam: String
2148
+ if let requestName = request.name, !requestName.trimmingCharacters(in: .whitespaces).isEmpty {
2149
+ nameParam = requestName
2150
+ } else {
2151
+ nameParam = emailPrefix
2152
+ }
2153
+
2154
+ var params: [String: Any] = [
2155
+ // "name": UserStoreSingleton.shared.merchantName ?? "",
2156
+ "name": nameParam,
2157
+ "account_id": accountID ?? "",
2158
+ "payment_method": "ach",
2159
+ "customer": customerID ?? "",
2160
+ "currency": "usd",
2161
+ "email": finalEmail
2162
+ ]
2163
+
2164
+ // Conditionally add billing info
2165
+ if let visibility = visibility, visibility.billing == true,
2166
+ let billing = billingInfo, !billing.isEmpty {
2167
+
2168
+ var billingInfoDict: [String: Any] = [:]
2169
+ for item in billing {
2170
+ billingInfoDict[item.name] = item.value
2171
+ }
2172
+
2173
+ params["address"] = billingInfoDict["address"] as? String ?? ""
2174
+ params["country"] = billingInfoDict["country"] as? String ?? ""
2175
+ params["state"] = billingInfoDict["state"] as? String ?? ""
2176
+ params["city"] = billingInfoDict["city"] as? String ?? ""
2177
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
2178
+ }
2179
+
2180
+ // Set default description if additional info is not visible
2181
+ if let visibility = visibility, visibility.additional == false {
2182
+ params["description"] = "Hosted payment checkout"
2183
+ }
2184
+
2185
+ // Add these if recurring is enabled
2186
+ // if let req = request, req.is_recurring == true {
2187
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
2188
+ // // Only send start_date if type is .custom and field is not empty
2189
+ // if let startDateText = startDate, !startDateText.isEmpty {
2190
+ // let inputFormatter = DateFormatter()
2191
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
2192
+ //
2193
+ // let outputFormatter = DateFormatter()
2194
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
2195
+ //
2196
+ // if let date = inputFormatter.date(from: startDateText) {
2197
+ // let apiFormattedDate = outputFormatter.string(from: date)
2198
+ // params["start_date"] = apiFormattedDate
2199
+ // } else {
2200
+ // }
2201
+ // }
2202
+ // }
2203
+ //
2204
+ // params["interval"] = chosenPlan?.lowercased()
2205
+ // }
2206
+
2207
+ // Add these if recurring is enabled
2208
+ if let req = request, req.is_recurring == true {
2209
+ if let startDateText = startDate, !startDateText.isEmpty {
2210
+ let inputFormatter = DateFormatter()
2211
+ inputFormatter.dateFormat = "dd/MM/yyyy"
2212
+
2213
+ let outputFormatter = DateFormatter()
2214
+ outputFormatter.dateFormat = "MM/dd/yyyy"
2215
+
2216
+ if let date = inputFormatter.date(from: startDateText) {
2217
+ let apiFormattedDate = outputFormatter.string(from: date)
2218
+ params["start_date"] = apiFormattedDate
2219
+ } else {
2220
+ }
2221
+ }
2222
+
2223
+ // interval is still required
2224
+ params["interval"] = chosenPlan?.lowercased()
2225
+ }
2226
+
2227
+ // ✅ Include metadata only if it has at least 1 key-value pair
2228
+ if let metadata = request?.metadata, !metadata.isEmpty {
2229
+ params["metadata"] = metadata
2230
+ }
2231
+
2232
+
2233
+ do {
2234
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
2235
+ uRLRequest.httpBody = jsonData
2236
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
2237
+ }
2238
+ } catch let error {
2239
+ hideLoadingIndicator()
2240
+ return
2241
+ }
2242
+
2243
+ let session = URLSession.shared
2244
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
2245
+
2246
+ DispatchQueue.main.async {
2247
+ self.hideLoadingIndicator() // Stop loader when response is received
2248
+ }
2249
+
2250
+ if let error = error {
2251
+ return
2252
+ }
2253
+
2254
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
2255
+ return
2256
+ }
2257
+
2258
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
2259
+ if let data = serviceData {
2260
+ do {
2261
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
2262
+
2263
+ // ✅ Handle duplicate transaction case
2264
+ if let status = responseObject["status"] as? Bool, status == false,
2265
+ let message = responseObject["message"] as? String,
2266
+ message.lowercased().contains("duplicate transaction") {
2267
+ self.presentPaymentErrorVC(errorMessage: message)
2268
+ return
2269
+ }
2270
+
2271
+ if let status = responseObject["status"] as? Int, status == 0,
2272
+ let message = responseObject["message"] as? String,
2273
+ message.lowercased().contains("duplicate transaction") {
2274
+ self.presentPaymentErrorVC(errorMessage: message)
2275
+ return
2276
+ }
2277
+
2278
+ // ✅ Handle generic "status == 0" error case
2279
+ if let status = responseObject["status"] as? Int, status == 0 {
2280
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
2281
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
2282
+ return
2283
+ }
2284
+ else {
2285
+ DispatchQueue.main.async {
2286
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
2287
+ paymentDoneVC.chargeData = responseObject
2288
+ // Pass the selected payment method
2289
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
2290
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate // Pass the delegate
2291
+ paymentDoneVC.bankPaymentParams = params
2292
+ // Pass billing and additional info
2293
+ // Conditionally pass raw FieldItem array
2294
+ paymentDoneVC.visibility = self.visibility
2295
+ paymentDoneVC.request = self.request
2296
+
2297
+ // if self.visibility?.billing == true {
2298
+ paymentDoneVC.billingInfoData = self.billingInfo
2299
+ var billingDict: [String: Any] = [:]
2300
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
2301
+ paymentDoneVC.billingInfo = billingDict
2302
+ // }
2303
+
2304
+ // if self.visibility?.additional == true {
2305
+ paymentDoneVC.additionalInfoData = self.additionalInfo
2306
+ var additionalDict: [String: Any] = [:]
2307
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
2308
+ paymentDoneVC.additionalInfo = additionalDict
2309
+ // }
2310
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
2311
+ }
2312
+ }
2313
+ }
2314
+ } else {
2315
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
2316
+ }
2317
+ } catch let jsonError {
2318
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
2319
+ }
2320
+ } else {
2321
+ self.presentPaymentErrorVC(errorMessage: "No data received")
2322
+ }
2323
+ } else {
2324
+ if let data = serviceData,
2325
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
2326
+ let message = responseObj["message"] as? String {
2327
+ self.presentPaymentErrorVC(errorMessage: message)
2328
+ } else {
2329
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
2330
+ }
2331
+ }
2332
+ }
2333
+ task.resume()
2334
+ }
2335
+
2336
+ //MARK: - Banking Account Charge Api
2337
+ func accountChargeApi() {
2338
+ showLoadingIndicator()
2339
+
2340
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.achCharge.path()
2341
+
2342
+ guard let serviceURL = URL(string: fullURL) else {
2343
+ hideLoadingIndicator()
2344
+ return
2345
+ }
2346
+
2347
+ var uRLRequest = URLRequest(url: serviceURL)
2348
+ uRLRequest.httpMethod = "POST"
2349
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
2350
+
2351
+ let token = UserStoreSingleton.shared.clientToken
2352
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
2353
+
2354
+ var params: [String: Any] = [
2355
+ //"name": accountName ?? "",
2356
+ "name": !(request.name?.isEmpty ?? true) ? request.name! : (accountName ?? ""),
2357
+ "email": userEmail ?? "",
2358
+ "currency": "usd",
2359
+ "account_type": accountType?.lowercased() ?? "",
2360
+ "routing_number": routingNumber ?? "",
2361
+ "account_number": accountNumber ?? "",
2362
+ "payment_mode": "auth_and_capture",
2363
+ "levelIndicator": 1,
2364
+ ]
2365
+
2366
+ // Conditionally add billing info
2367
+ if let visibility = visibility, visibility.billing == true,
2368
+ let billing = billingInfo, !billing.isEmpty {
2369
+
2370
+ var billingInfoDict: [String: Any] = [:]
2371
+ for item in billing {
2372
+ billingInfoDict[item.name] = item.value
2373
+ }
2374
+
2375
+ params["address"] = billingInfoDict["address"] as? String ?? ""
2376
+ params["country"] = billingInfoDict["country"] as? String ?? ""
2377
+ params["state"] = billingInfoDict["state"] as? String ?? ""
2378
+ params["city"] = billingInfoDict["city"] as? String ?? ""
2379
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
2380
+ }
2381
+
2382
+ // Set default description if additional info is not visible
2383
+ if let visibility = visibility, visibility.additional == false {
2384
+ params["description"] = "Hosted payment checkout"
2385
+ }
2386
+
2387
+ // Add these if recurring is enabled
2388
+ // if let req = request, req.is_recurring == true {
2389
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
2390
+ // // Only send start_date if type is .custom and field is not empty
2391
+ // if let startDateText = startDate, !startDateText.isEmpty {
2392
+ // let inputFormatter = DateFormatter()
2393
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
2394
+ //
2395
+ // let outputFormatter = DateFormatter()
2396
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
2397
+ //
2398
+ // if let date = inputFormatter.date(from: startDateText) {
2399
+ // let apiFormattedDate = outputFormatter.string(from: date)
2400
+ // params["start_date"] = apiFormattedDate
2401
+ // } else {
2402
+ // }
2403
+ // }
2404
+ // }
2405
+ //
2406
+ // params["interval"] = chosenPlan?.lowercased()
2407
+ // }
2408
+
2409
+ // Add these if recurring is enabled
2410
+ if let req = request, req.is_recurring == true {
2411
+ if let startDateText = startDate, !startDateText.isEmpty {
2412
+ let inputFormatter = DateFormatter()
2413
+ inputFormatter.dateFormat = "dd/MM/yyyy"
2414
+
2415
+ let outputFormatter = DateFormatter()
2416
+ outputFormatter.dateFormat = "MM/dd/yyyy"
2417
+
2418
+ if let date = inputFormatter.date(from: startDateText) {
2419
+ let apiFormattedDate = outputFormatter.string(from: date)
2420
+ params["start_date"] = apiFormattedDate
2421
+ } else {
2422
+ }
2423
+ }
2424
+
2425
+ // interval is still required
2426
+ params["interval"] = chosenPlan?.lowercased()
2427
+ }
2428
+
2429
+ // ✅ Include metadata only if it has at least 1 key-value pair
2430
+ if let metadata = request?.metadata, !metadata.isEmpty {
2431
+ params["metadata"] = metadata
2432
+ }
2433
+
2434
+
2435
+ do {
2436
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
2437
+ uRLRequest.httpBody = jsonData
2438
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
2439
+ }
2440
+ } catch let error {
2441
+ hideLoadingIndicator()
2442
+ return
2443
+ }
2444
+
2445
+ let session = URLSession.shared
2446
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
2447
+
2448
+ DispatchQueue.main.async {
2449
+ self.hideLoadingIndicator() // Stop loader when response is received
2450
+ }
2451
+
2452
+ if let error = error {
2453
+ return
2454
+ }
2455
+
2456
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
2457
+ return
2458
+ }
2459
+
2460
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
2461
+ if let data = serviceData {
2462
+ do {
2463
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
2464
+
2465
+ // ✅ Handle duplicate transaction case
2466
+ if let status = responseObject["status"] as? Bool, status == false,
2467
+ let message = responseObject["message"] as? String,
2468
+ message.lowercased().contains("duplicate transaction") {
2469
+ self.presentPaymentErrorVC(errorMessage: message)
2470
+ return
2471
+ }
2472
+
2473
+ if let status = responseObject["status"] as? Int, status == 0,
2474
+ let message = responseObject["message"] as? String,
2475
+ message.lowercased().contains("duplicate transaction") {
2476
+ self.presentPaymentErrorVC(errorMessage: message)
2477
+ return
2478
+ }
2479
+
2480
+ // ✅ Handle generic "status == 0" error case
2481
+ if let status = responseObject["status"] as? Int, status == 0 {
2482
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
2483
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
2484
+ return
2485
+ }
2486
+ else {
2487
+ DispatchQueue.main.async {
2488
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
2489
+ paymentDoneVC.chargeData = responseObject
2490
+ // Pass the selected payment method
2491
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
2492
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate // Pass the delegate
2493
+ paymentDoneVC.bankPaymentParams = params
2494
+ // Pass billing and additional info
2495
+ // Conditionally pass raw FieldItem array
2496
+ paymentDoneVC.visibility = self.visibility
2497
+ paymentDoneVC.request = self.request
2498
+
2499
+ // if self.visibility?.billing == true {
2500
+ paymentDoneVC.billingInfoData = self.billingInfo
2501
+ var billingDict: [String: Any] = [:]
2502
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
2503
+ paymentDoneVC.billingInfo = billingDict
2504
+ // }
2505
+
2506
+ // if self.visibility?.additional == true {
2507
+ paymentDoneVC.additionalInfoData = self.additionalInfo
2508
+ var additionalDict: [String: Any] = [:]
2509
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
2510
+ paymentDoneVC.additionalInfo = additionalDict
2511
+ // }
2512
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
2513
+ }
2514
+ }
2515
+ }
2516
+ } else {
2517
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
2518
+ }
2519
+ } catch let jsonError {
2520
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
2521
+ }
2522
+ } else {
2523
+ self.presentPaymentErrorVC(errorMessage: "No data received")
2524
+ }
2525
+ } else {
2526
+ if let data = serviceData,
2527
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
2528
+ let message = responseObj["message"] as? String {
2529
+ self.presentPaymentErrorVC(errorMessage: message)
2530
+ } else {
2531
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
2532
+ }
2533
+ }
2534
+ }
2535
+ task.resume()
2536
+ }
2537
+
2538
+ //MARK: - Account Charge Api if user saved the account.
2539
+ func accountChargeApi(customerId: String?) {
2540
+ showLoadingIndicator()
2541
+
2542
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.achCharge.path()
2543
+
2544
+ guard let serviceURL = URL(string: fullURL) else {
2545
+ hideLoadingIndicator()
2546
+ return
2547
+ }
2548
+
2549
+ var uRLRequest = URLRequest(url: serviceURL)
2550
+ uRLRequest.httpMethod = "POST"
2551
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
2552
+
2553
+ let token = UserStoreSingleton.shared.clientToken
2554
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
2555
+
2556
+ // let emailPrefix = UserStoreSingleton.shared.verificationEmail?.components(separatedBy: "@").first ?? ""
2557
+
2558
+ let finalEmail: String
2559
+ if let verificationEmail = UserStoreSingleton.shared.verificationEmail, !verificationEmail.isEmpty {
2560
+ finalEmail = verificationEmail
2561
+ } else {
2562
+ finalEmail = request.email ?? ""
2563
+ }
2564
+
2565
+ let emailPrefix: String
2566
+ if !finalEmail.isEmpty {
2567
+ emailPrefix = finalEmail.components(separatedBy: "@").first ?? ""
2568
+ } else {
2569
+ emailPrefix = ""
2570
+ }
2571
+
2572
+ var params: [String: Any] = [
2573
+ // "name": accountName ?? "",
2574
+ "name": !(request.name?.isEmpty ?? true) ? request.name! : (accountName ?? ""),
2575
+ "email": userEmail ?? "",
2576
+ "currency": "usd",
2577
+ "account_type": accountType?.lowercased() ?? "",
2578
+ "routing_number": routingNumber ?? "",
2579
+ "account_number": accountNumber ?? "",
2580
+ "payment_mode": "auth_and_capture",
2581
+ "levelIndicator": 1,
2582
+ "save_account": (isSavedNewAccount ?? false) ? 1 : 0,
2583
+ "payment_method": "ach"
2584
+ ]
2585
+
2586
+ if let customerId = customerId {
2587
+ params["customer"] = customerId
2588
+ } else {
2589
+ params["username"] = emailPrefix
2590
+ }
2591
+
2592
+ // Conditionally add billing info
2593
+ if let visibility = visibility, visibility.billing == true,
2594
+ let billing = billingInfo, !billing.isEmpty {
2595
+
2596
+ var billingInfoDict: [String: Any] = [:]
2597
+ for item in billing {
2598
+ billingInfoDict[item.name] = item.value
2599
+ }
2600
+
2601
+ params["address"] = billingInfoDict["address"] as? String ?? ""
2602
+ params["country"] = billingInfoDict["country"] as? String ?? ""
2603
+ params["state"] = billingInfoDict["state"] as? String ?? ""
2604
+ params["city"] = billingInfoDict["city"] as? String ?? ""
2605
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
2606
+ }
2607
+
2608
+ // Set default description if additional info is not visible
2609
+ if let visibility = visibility, visibility.additional == false {
2610
+ params["description"] = "Hosted payment checkout"
2611
+ }
2612
+
2613
+ // Add these if recurring is enabled
2614
+ // if let req = request, req.is_recurring == true {
2615
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
2616
+ // // Only send start_date if type is .custom and field is not empty
2617
+ // if let startDateText = startDate, !startDateText.isEmpty {
2618
+ // let inputFormatter = DateFormatter()
2619
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
2620
+ //
2621
+ // let outputFormatter = DateFormatter()
2622
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
2623
+ //
2624
+ // if let date = inputFormatter.date(from: startDateText) {
2625
+ // let apiFormattedDate = outputFormatter.string(from: date)
2626
+ // params["start_date"] = apiFormattedDate
2627
+ // } else {
2628
+ // }
2629
+ // }
2630
+ // }
2631
+ //
2632
+ // params["interval"] = chosenPlan?.lowercased()
2633
+ // }
2634
+
2635
+ // Add these if recurring is enabled
2636
+ if let req = request, req.is_recurring == true {
2637
+ if let startDateText = startDate, !startDateText.isEmpty {
2638
+ let inputFormatter = DateFormatter()
2639
+ inputFormatter.dateFormat = "dd/MM/yyyy"
2640
+
2641
+ let outputFormatter = DateFormatter()
2642
+ outputFormatter.dateFormat = "MM/dd/yyyy"
2643
+
2644
+ if let date = inputFormatter.date(from: startDateText) {
2645
+ let apiFormattedDate = outputFormatter.string(from: date)
2646
+ params["start_date"] = apiFormattedDate
2647
+ } else {
2648
+ }
2649
+ }
2650
+
2651
+ // interval is still required
2652
+ params["interval"] = chosenPlan?.lowercased()
2653
+ }
2654
+
2655
+ // ✅ Include metadata only if it has at least 1 key-value pair
2656
+ if let metadata = request?.metadata, !metadata.isEmpty {
2657
+ params["metadata"] = metadata
2658
+ }
2659
+
2660
+
2661
+ do {
2662
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
2663
+ uRLRequest.httpBody = jsonData
2664
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
2665
+ }
2666
+ } catch let error {
2667
+ hideLoadingIndicator()
2668
+ return
2669
+ }
2670
+
2671
+ let session = URLSession.shared
2672
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
2673
+
2674
+ DispatchQueue.main.async {
2675
+ self.hideLoadingIndicator() // Stop loader when response is received
2676
+ }
2677
+
2678
+ if let error = error {
2679
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
2680
+ return
2681
+ }
2682
+
2683
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
2684
+ self.presentPaymentErrorVC(errorMessage: "Invalid response")
2685
+ return
2686
+ }
2687
+
2688
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
2689
+ if let data = serviceData {
2690
+ do {
2691
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
2692
+
2693
+ // ✅ Handle duplicate transaction case
2694
+ if let status = responseObject["status"] as? Bool, status == false,
2695
+ let message = responseObject["message"] as? String,
2696
+ message.lowercased().contains("duplicate transaction") {
2697
+ self.presentPaymentErrorVC(errorMessage: message)
2698
+ return
2699
+ }
2700
+
2701
+ if let status = responseObject["status"] as? Int, status == 0,
2702
+ let message = responseObject["message"] as? String,
2703
+ message.lowercased().contains("duplicate transaction") {
2704
+ self.presentPaymentErrorVC(errorMessage: message)
2705
+ return
2706
+ }
2707
+
2708
+ // ✅ Handle generic "status == 0" error case
2709
+ if let status = responseObject["status"] as? Int, status == 0 {
2710
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
2711
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
2712
+ return
2713
+ }
2714
+ else {
2715
+ DispatchQueue.main.async {
2716
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
2717
+ paymentDoneVC.chargeData = responseObject
2718
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
2719
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
2720
+ paymentDoneVC.bankPaymentParams = params
2721
+ // Pass billing and additional info
2722
+ // Conditionally pass raw FieldItem array
2723
+ paymentDoneVC.visibility = self.visibility
2724
+ paymentDoneVC.request = self.request
2725
+
2726
+ // if self.visibility?.billing == true {
2727
+ paymentDoneVC.billingInfoData = self.billingInfo
2728
+ var billingDict: [String: Any] = [:]
2729
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
2730
+ paymentDoneVC.billingInfo = billingDict
2731
+ // }
2732
+
2733
+ // if self.visibility?.additional == true {
2734
+ paymentDoneVC.additionalInfoData = self.additionalInfo
2735
+ var additionalDict: [String: Any] = [:]
2736
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
2737
+ paymentDoneVC.additionalInfo = additionalDict
2738
+ // }
2739
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
2740
+ }
2741
+ }
2742
+ }
2743
+ } else {
2744
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
2745
+ }
2746
+ } catch let jsonError {
2747
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
2748
+ }
2749
+ } else {
2750
+ self.presentPaymentErrorVC(errorMessage: "No data received")
2751
+ }
2752
+ } else {
2753
+ if let data = serviceData,
2754
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
2755
+ let message = responseObj["message"] as? String {
2756
+ self.presentPaymentErrorVC(errorMessage: message)
2757
+ } else {
2758
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
2759
+ }
2760
+ }
2761
+ }
2762
+ task.resume()
2763
+ }
2764
+
2765
+ //MARK: - Account Charge Api if user logged in and saved the account.
2766
+ func accountChargeWithSaveApi(customerId: String?) {
2767
+ showLoadingIndicator()
2768
+
2769
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.achCharge.path()
2770
+
2771
+ guard let serviceURL = URL(string: fullURL) else {
2772
+ hideLoadingIndicator()
2773
+ return
2774
+ }
2775
+
2776
+ var uRLRequest = URLRequest(url: serviceURL)
2777
+ uRLRequest.httpMethod = "POST"
2778
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
2779
+
2780
+ let token = UserStoreSingleton.shared.clientToken
2781
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
2782
+
2783
+ // let emailPrefix = UserStoreSingleton.shared.verificationEmail?.components(separatedBy: "@").first ?? ""
2784
+
2785
+ let finalEmail: String
2786
+ if let verificationEmail = UserStoreSingleton.shared.verificationEmail, !verificationEmail.isEmpty {
2787
+ finalEmail = verificationEmail
2788
+ } else {
2789
+ finalEmail = request.email ?? ""
2790
+ }
2791
+
2792
+ let emailPrefix: String
2793
+ if !finalEmail.isEmpty {
2794
+ emailPrefix = finalEmail.components(separatedBy: "@").first ?? ""
2795
+ } else {
2796
+ emailPrefix = ""
2797
+ }
2798
+
2799
+ var params: [String: Any] = [
2800
+ // "name": accountName ?? "",
2801
+ "name": !(request.name?.isEmpty ?? true) ? request.name! : (accountName ?? ""),
2802
+ "email": userEmail ?? "",
2803
+ "currency": "usd",
2804
+ "account_type": accountType?.lowercased() ?? "",
2805
+ "routing_number": routingNumber ?? "",
2806
+ "account_number": accountNumber ?? "",
2807
+ "payment_mode": "auth_and_capture",
2808
+ "levelIndicator": 1,
2809
+ "save_account": (isSavedNewAccount ?? false) ? 1 : 0,
2810
+ "payment_method": "ach"
2811
+ ]
2812
+
2813
+ if let customerId = customerId {
2814
+ params["customer"] = customerId
2815
+ } else {
2816
+ params["username"] = emailPrefix
2817
+ }
2818
+
2819
+ if customerId == nil {
2820
+ params["create_customer"] = "1"
2821
+ }
2822
+
2823
+ // Conditionally add billing info
2824
+ if let visibility = visibility, visibility.billing == true,
2825
+ let billing = billingInfo, !billing.isEmpty {
2826
+
2827
+ var billingInfoDict: [String: Any] = [:]
2828
+ for item in billing {
2829
+ billingInfoDict[item.name] = item.value
2830
+ }
2831
+
2832
+ params["address"] = billingInfoDict["address"] as? String ?? ""
2833
+ params["country"] = billingInfoDict["country"] as? String ?? ""
2834
+ params["state"] = billingInfoDict["state"] as? String ?? ""
2835
+ params["city"] = billingInfoDict["city"] as? String ?? ""
2836
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
2837
+ }
2838
+
2839
+ // Set default description if additional info is not visible
2840
+ if let visibility = visibility, visibility.additional == false {
2841
+ params["description"] = "Hosted payment checkout"
2842
+ }
2843
+
2844
+ // Add these if recurring is enabled
2845
+ // if let req = request, req.is_recurring == true {
2846
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
2847
+ // // Only send start_date if type is .custom and field is not empty
2848
+ // if let startDateText = startDate, !startDateText.isEmpty {
2849
+ // let inputFormatter = DateFormatter()
2850
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
2851
+ //
2852
+ // let outputFormatter = DateFormatter()
2853
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
2854
+ //
2855
+ // if let date = inputFormatter.date(from: startDateText) {
2856
+ // let apiFormattedDate = outputFormatter.string(from: date)
2857
+ // params["start_date"] = apiFormattedDate
2858
+ // } else {
2859
+ // }
2860
+ // }
2861
+ // }
2862
+ //
2863
+ // params["interval"] = chosenPlan?.lowercased()
2864
+ // }
2865
+
2866
+ // Add these if recurring is enabled
2867
+ if let req = request, req.is_recurring == true {
2868
+ if let startDateText = startDate, !startDateText.isEmpty {
2869
+ let inputFormatter = DateFormatter()
2870
+ inputFormatter.dateFormat = "dd/MM/yyyy"
2871
+
2872
+ let outputFormatter = DateFormatter()
2873
+ outputFormatter.dateFormat = "MM/dd/yyyy"
2874
+
2875
+ if let date = inputFormatter.date(from: startDateText) {
2876
+ let apiFormattedDate = outputFormatter.string(from: date)
2877
+ params["start_date"] = apiFormattedDate
2878
+ } else {
2879
+ }
2880
+ }
2881
+
2882
+ // interval is still required
2883
+ params["interval"] = chosenPlan?.lowercased()
2884
+ }
2885
+
2886
+ // ✅ Include metadata only if it has at least 1 key-value pair
2887
+ if let metadata = request?.metadata, !metadata.isEmpty {
2888
+ params["metadata"] = metadata
2889
+ }
2890
+
2891
+
2892
+ do {
2893
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
2894
+ uRLRequest.httpBody = jsonData
2895
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
2896
+ }
2897
+ } catch let error {
2898
+ hideLoadingIndicator()
2899
+ return
2900
+ }
2901
+
2902
+ let session = URLSession.shared
2903
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
2904
+
2905
+ DispatchQueue.main.async {
2906
+ self.hideLoadingIndicator() // Stop loader when response is received
2907
+ }
2908
+
2909
+ if let error = error {
2910
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
2911
+ return
2912
+ }
2913
+
2914
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
2915
+ self.presentPaymentErrorVC(errorMessage: "Invalid response")
2916
+ return
2917
+ }
2918
+
2919
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
2920
+ if let data = serviceData {
2921
+ do {
2922
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
2923
+
2924
+ // ✅ Handle duplicate transaction case
2925
+ if let status = responseObject["status"] as? Bool, status == false,
2926
+ let message = responseObject["message"] as? String,
2927
+ message.lowercased().contains("duplicate transaction") {
2928
+ self.presentPaymentErrorVC(errorMessage: message)
2929
+ return
2930
+ }
2931
+
2932
+ if let status = responseObject["status"] as? Int, status == 0,
2933
+ let message = responseObject["message"] as? String,
2934
+ message.lowercased().contains("duplicate transaction") {
2935
+ self.presentPaymentErrorVC(errorMessage: message)
2936
+ return
2937
+ }
2938
+
2939
+ // ✅ Handle generic "status == 0" error case
2940
+ if let status = responseObject["status"] as? Int, status == 0 {
2941
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
2942
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
2943
+ return
2944
+ }
2945
+ else {
2946
+ DispatchQueue.main.async {
2947
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
2948
+ paymentDoneVC.chargeData = responseObject
2949
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
2950
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
2951
+ paymentDoneVC.bankPaymentParams = params
2952
+ // Pass billing and additional info
2953
+ // Conditionally pass raw FieldItem array
2954
+ paymentDoneVC.visibility = self.visibility
2955
+ paymentDoneVC.request = self.request
2956
+
2957
+ // if self.visibility?.billing == true {
2958
+ paymentDoneVC.billingInfoData = self.billingInfo
2959
+ var billingDict: [String: Any] = [:]
2960
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
2961
+ paymentDoneVC.billingInfo = billingDict
2962
+ // }
2963
+
2964
+ // if self.visibility?.additional == true {
2965
+ paymentDoneVC.additionalInfoData = self.additionalInfo
2966
+ var additionalDict: [String: Any] = [:]
2967
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
2968
+ paymentDoneVC.additionalInfo = additionalDict
2969
+ // }
2970
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
2971
+ }
2972
+ }
2973
+ }
2974
+ } else {
2975
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
2976
+ }
2977
+ } catch let jsonError {
2978
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
2979
+ }
2980
+ } else {
2981
+ self.presentPaymentErrorVC(errorMessage: "No data received")
2982
+ }
2983
+ } else {
2984
+ if let data = serviceData,
2985
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
2986
+ let message = responseObj["message"] as? String {
2987
+ self.presentPaymentErrorVC(errorMessage: message)
2988
+ } else {
2989
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
2990
+ }
2991
+ }
2992
+ }
2993
+ task.resume()
2994
+ }
2995
+
2996
+ //MARK: - GrailPay Account Charge Api if user not saved account but billing info available
2997
+ func grailPayAccountChargeApi() {
2998
+ showLoadingIndicator()
2999
+
3000
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.achCharge.path()
3001
+
3002
+ guard let serviceURL = URL(string: fullURL) else {
3003
+ hideLoadingIndicator()
3004
+ return
3005
+ }
3006
+
3007
+ var uRLRequest = URLRequest(url: serviceURL)
3008
+ uRLRequest.httpMethod = "POST"
3009
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
3010
+
3011
+ let token = UserStoreSingleton.shared.clientToken
3012
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
3013
+
3014
+ var params: [String: Any] = [
3015
+ "account_id": self.grailPayAccountID ?? "",
3016
+ "account_type": self.selectedGrailPayAccountType ?? "",
3017
+ "name": self.selectedGrailPayAccountName ?? "",
3018
+ "description": "payment checkout",
3019
+ "email": userEmail ?? ""
3020
+ ]
3021
+
3022
+ // ✅ Only add these params for logged-in users
3023
+ if UserStoreSingleton.shared.isLoggedIn == true {
3024
+ let emailText = userEmail
3025
+ let emailPrefix = emailText?.components(separatedBy: "@").first ?? ""
3026
+
3027
+ params["save_account"] = (isSavedNewAccount ?? false) ? 1 : 0
3028
+ params["is_default"] = (isSavedNewAccount ?? false) ? 1 : 0
3029
+ params["customer_id"] = UserStoreSingleton.shared.customerId ?? ""
3030
+
3031
+ if let customerId = UserStoreSingleton.shared.customerId, !customerId.isEmpty {
3032
+ params["customer"] = customerId
3033
+ } else {
3034
+ params["username"] = emailPrefix
3035
+ }
3036
+
3037
+ if UserStoreSingleton.shared.customerId == nil {
3038
+ params["create_customer"] = "1"
3039
+ }
3040
+ }
3041
+
3042
+ // Conditionally add billing info
3043
+ if let visibility = visibility, visibility.billing == true,
3044
+ let billing = billingInfo, !billing.isEmpty {
3045
+
3046
+ var billingInfoDict: [String: Any] = [:]
3047
+ for item in billing {
3048
+ billingInfoDict[item.name] = item.value
3049
+ }
3050
+
3051
+ params["address"] = billingInfoDict["address"] as? String ?? ""
3052
+ params["country"] = billingInfoDict["country"] as? String ?? ""
3053
+ params["state"] = billingInfoDict["state"] as? String ?? ""
3054
+ params["city"] = billingInfoDict["city"] as? String ?? ""
3055
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
3056
+ }
3057
+
3058
+ // Set default description if additional info is not visible
3059
+ if let visibility = visibility, visibility.additional == false {
3060
+ params["description"] = "Hosted payment checkout"
3061
+ }
3062
+
3063
+ // Add these if recurring is enabled
3064
+ // if let req = request, req.is_recurring == true {
3065
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
3066
+ // // Only send start_date if type is .custom and field is not empty
3067
+ // if let startDateText = startDate, !startDateText.isEmpty {
3068
+ // let inputFormatter = DateFormatter()
3069
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
3070
+ //
3071
+ // let outputFormatter = DateFormatter()
3072
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
3073
+ //
3074
+ // if let date = inputFormatter.date(from: startDateText) {
3075
+ // let apiFormattedDate = outputFormatter.string(from: date)
3076
+ // params["start_date"] = apiFormattedDate
3077
+ // } else {
3078
+ // }
3079
+ // }
3080
+ // }
3081
+ //
3082
+ // params["interval"] = chosenPlan?.lowercased()
3083
+ // }
3084
+
3085
+ // Add these if recurring is enabled
3086
+ if let req = request, req.is_recurring == true {
3087
+ if let startDateText = startDate, !startDateText.isEmpty {
3088
+ let inputFormatter = DateFormatter()
3089
+ inputFormatter.dateFormat = "dd/MM/yyyy"
3090
+
3091
+ let outputFormatter = DateFormatter()
3092
+ outputFormatter.dateFormat = "MM/dd/yyyy"
3093
+
3094
+ if let date = inputFormatter.date(from: startDateText) {
3095
+ let apiFormattedDate = outputFormatter.string(from: date)
3096
+ params["start_date"] = apiFormattedDate
3097
+ } else {
3098
+ }
3099
+ }
3100
+
3101
+ // interval is still required
3102
+ params["interval"] = chosenPlan?.lowercased()
3103
+ }
3104
+
3105
+ // ✅ Include metadata only if it has at least 1 key-value pair
3106
+ if let metadata = request?.metadata, !metadata.isEmpty {
3107
+ params["metadata"] = metadata
3108
+ }
3109
+
3110
+
3111
+ do {
3112
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
3113
+ uRLRequest.httpBody = jsonData
3114
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
3115
+ }
3116
+ } catch let error {
3117
+ hideLoadingIndicator()
3118
+ return
3119
+ }
3120
+
3121
+ let session = URLSession.shared
3122
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
3123
+
3124
+ DispatchQueue.main.async {
3125
+ self.hideLoadingIndicator() // Stop loader when response is received
3126
+ }
3127
+
3128
+ if let error = error {
3129
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
3130
+ return
3131
+ }
3132
+
3133
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
3134
+ self.presentPaymentErrorVC(errorMessage: "Invalid response")
3135
+ return
3136
+ }
3137
+
3138
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
3139
+ if let data = serviceData {
3140
+ do {
3141
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
3142
+
3143
+ // ✅ Handle duplicate transaction case
3144
+ if let status = responseObject["status"] as? Bool, status == false,
3145
+ let message = responseObject["message"] as? String,
3146
+ message.lowercased().contains("duplicate transaction") {
3147
+ self.presentPaymentErrorVC(errorMessage: message)
3148
+ return
3149
+ }
3150
+
3151
+ if let status = responseObject["status"] as? Int, status == 0,
3152
+ let message = responseObject["message"] as? String,
3153
+ message.lowercased().contains("duplicate transaction") {
3154
+ self.presentPaymentErrorVC(errorMessage: message)
3155
+ return
3156
+ }
3157
+
3158
+ // ✅ Handle generic "status == 0" error case
3159
+ if let status = responseObject["status"] as? Int, status == 0 {
3160
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
3161
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
3162
+ return
3163
+ }
3164
+ else {
3165
+ DispatchQueue.main.async {
3166
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
3167
+ paymentDoneVC.chargeData = responseObject
3168
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
3169
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
3170
+ paymentDoneVC.bankPaymentParams = params
3171
+ // Pass billing and additional info
3172
+ // Conditionally pass raw FieldItem array
3173
+ paymentDoneVC.visibility = self.visibility
3174
+ paymentDoneVC.request = self.request
3175
+
3176
+ // if self.visibility?.billing == true {
3177
+ paymentDoneVC.billingInfoData = self.billingInfo
3178
+ var billingDict: [String: Any] = [:]
3179
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
3180
+ paymentDoneVC.billingInfo = billingDict
3181
+ // }
3182
+
3183
+ // if self.visibility?.additional == true {
3184
+ paymentDoneVC.additionalInfoData = self.additionalInfo
3185
+ var additionalDict: [String: Any] = [:]
3186
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
3187
+ paymentDoneVC.additionalInfo = additionalDict
3188
+ // }
3189
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
3190
+ }
3191
+ }
3192
+ }
3193
+ } else {
3194
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
3195
+ }
3196
+ } catch let jsonError {
3197
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
3198
+ }
3199
+ } else {
3200
+ self.presentPaymentErrorVC(errorMessage: "No data received")
3201
+ }
3202
+ } else {
3203
+ if let data = serviceData,
3204
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
3205
+ let message = responseObj["message"] as? String {
3206
+ self.presentPaymentErrorVC(errorMessage: message)
3207
+ } else {
3208
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
3209
+ }
3210
+ }
3211
+ }
3212
+ task.resume()
3213
+ }
3214
+
3215
+ //MARK: - GrailPay Account Charge Api if user saved account
3216
+ func grailPayAccountChargeApi(customerId: String?) {
3217
+ showLoadingIndicator()
3218
+
3219
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.achCharge.path()
3220
+
3221
+ guard let serviceURL = URL(string: fullURL) else {
3222
+ hideLoadingIndicator()
3223
+ return
3224
+ }
3225
+
3226
+ var uRLRequest = URLRequest(url: serviceURL)
3227
+ uRLRequest.httpMethod = "POST"
3228
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
3229
+
3230
+ let token = UserStoreSingleton.shared.clientToken
3231
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
3232
+
3233
+ let emailPrefix = userEmail?.components(separatedBy: "@").first ?? ""
3234
+
3235
+ var params: [String: Any] = [
3236
+ "account_id": self.grailPayAccountID ?? "",
3237
+ "account_type": self.selectedGrailPayAccountType ?? "",
3238
+ "name": self.selectedGrailPayAccountName ?? "",
3239
+ "save_account": (isSavedForFuture ?? false) ? 1 : 0,
3240
+ "is_default": (isSavedForFuture ?? false) ? 1 : 0,
3241
+ "customer_id": customerId ?? "",
3242
+ "email": userEmail ?? "",
3243
+ "create_customer": "1",
3244
+ ]
3245
+
3246
+ if let customerId = customerId {
3247
+ params["customer"] = customerId
3248
+ } else {
3249
+ params["username"] = emailPrefix
3250
+ }
3251
+
3252
+ // // Billing Info
3253
+ // if let visibility = visibility, visibility.billing == true,
3254
+ // let billing = billingInfo, !billing.isEmpty {
3255
+ // var billingDict: [String: Any] = [:]
3256
+ // billing.forEach { billingDict[$0.name] = $0.value }
3257
+ //
3258
+ // params["address"] = billingDict["address"] as? String ?? ""
3259
+ // params["country"] = billingDict["country"] as? String ?? ""
3260
+ // params["state"] = billingDict["state"] as? String ?? ""
3261
+ // params["city"] = billingDict["city"] as? String ?? ""
3262
+ // params["zip"] = billingDict["postal_code"] as? String ?? ""
3263
+ // }
3264
+
3265
+ // Always include Billing Info if available
3266
+ if let billing = billingInfo, !billing.isEmpty {
3267
+ var billingDict: [String: Any] = [:]
3268
+ billing.forEach { billingDict[$0.name] = $0.value }
3269
+
3270
+ params["address"] = billingDict["address"] as? String ?? ""
3271
+ params["country"] = billingDict["country"] as? String ?? ""
3272
+ params["state"] = billingDict["state"] as? String ?? ""
3273
+ params["city"] = billingDict["city"] as? String ?? ""
3274
+ params["zip"] = billingDict["postal_code"] as? String ?? ""
3275
+ }
3276
+
3277
+ // // Additional Info or default description
3278
+ // var descriptionValue: String = "Hosted payment checkout" // default
3279
+ // if let visibility = visibility, visibility.additional == true,
3280
+ // let additional = additionalInfo, !additional.isEmpty {
3281
+ //
3282
+ // var additionalDict: [String: Any] = [:]
3283
+ // additional.forEach { additionalDict[$0.name] = $0.value }
3284
+ //
3285
+ // if let desc = additionalDict["description"] as? String, !desc.isEmpty {
3286
+ // descriptionValue = desc
3287
+ // }
3288
+ //
3289
+ // if let phone = additionalDict["phone_number"] as? String, !phone.isEmpty {
3290
+ // params["phone_number"] = phone
3291
+ // }
3292
+ // }
3293
+ // params["description"] = descriptionValue
3294
+
3295
+ // Always include Additional Info if available
3296
+ var descriptionValue: String = "Hosted payment checkout"
3297
+ if let additional = additionalInfo, !additional.isEmpty {
3298
+ var additionalDict: [String: Any] = [:]
3299
+ additional.forEach { additionalDict[$0.name] = $0.value }
3300
+
3301
+ if let desc = additionalDict["description"] as? String, !desc.isEmpty {
3302
+ descriptionValue = desc
3303
+ }
3304
+
3305
+ if let phone = additionalDict["phone_number"] as? String, !phone.isEmpty {
3306
+ params["phone_number"] = phone
3307
+ }
3308
+ }
3309
+ params["description"] = descriptionValue
3310
+
3311
+ // Add these if recurring is enabled
3312
+ // if let req = request, req.is_recurring == true {
3313
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
3314
+ // // Only send start_date if type is .custom and field is not empty
3315
+ // if let startDateText = startDate, !startDateText.isEmpty {
3316
+ // let inputFormatter = DateFormatter()
3317
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
3318
+ //
3319
+ // let outputFormatter = DateFormatter()
3320
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
3321
+ //
3322
+ // if let date = inputFormatter.date(from: startDateText) {
3323
+ // let apiFormattedDate = outputFormatter.string(from: date)
3324
+ // params["start_date"] = apiFormattedDate
3325
+ // } else {
3326
+ // }
3327
+ // }
3328
+ // }
3329
+ //
3330
+ // params["interval"] = chosenPlan?.lowercased()
3331
+ // }
3332
+
3333
+ // Add these if recurring is enabled
3334
+ if let req = request, req.is_recurring == true {
3335
+ if let startDateText = startDate, !startDateText.isEmpty {
3336
+ let inputFormatter = DateFormatter()
3337
+ inputFormatter.dateFormat = "dd/MM/yyyy"
3338
+
3339
+ let outputFormatter = DateFormatter()
3340
+ outputFormatter.dateFormat = "MM/dd/yyyy"
3341
+
3342
+ if let date = inputFormatter.date(from: startDateText) {
3343
+ let apiFormattedDate = outputFormatter.string(from: date)
3344
+ params["start_date"] = apiFormattedDate
3345
+ } else {
3346
+ }
3347
+ }
3348
+
3349
+ // interval is still required
3350
+ params["interval"] = chosenPlan?.lowercased()
3351
+ }
3352
+
3353
+ // ✅ Include metadata only if it has at least 1 key-value pair
3354
+ if let metadata = request?.metadata, !metadata.isEmpty {
3355
+ params["metadata"] = metadata
3356
+ }
3357
+
3358
+
3359
+ do {
3360
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
3361
+ uRLRequest.httpBody = jsonData
3362
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
3363
+ }
3364
+ } catch let error {
3365
+ hideLoadingIndicator()
3366
+ return
3367
+ }
3368
+
3369
+ let session = URLSession.shared
3370
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
3371
+
3372
+ DispatchQueue.main.async {
3373
+ self.hideLoadingIndicator() // Stop loader when response is received
3374
+ }
3375
+
3376
+ if let error = error {
3377
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
3378
+ return
3379
+ }
3380
+
3381
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
3382
+ self.presentPaymentErrorVC(errorMessage: "Invalid response")
3383
+ return
3384
+ }
3385
+
3386
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
3387
+ if let data = serviceData {
3388
+ do {
3389
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
3390
+
3391
+ // ✅ Handle duplicate transaction case
3392
+ if let status = responseObject["status"] as? Bool, status == false,
3393
+ let message = responseObject["message"] as? String,
3394
+ message.lowercased().contains("duplicate transaction") {
3395
+ self.presentPaymentErrorVC(errorMessage: message)
3396
+ return
3397
+ }
3398
+
3399
+ if let status = responseObject["status"] as? Int, status == 0,
3400
+ let message = responseObject["message"] as? String,
3401
+ message.lowercased().contains("duplicate transaction") {
3402
+ self.presentPaymentErrorVC(errorMessage: message)
3403
+ return
3404
+ }
3405
+
3406
+ // ✅ Handle generic "status == 0" error case
3407
+ if let status = responseObject["status"] as? Int, status == 0 {
3408
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
3409
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
3410
+ return
3411
+ }
3412
+ else {
3413
+ DispatchQueue.main.async {
3414
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
3415
+ paymentDoneVC.chargeData = responseObject
3416
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
3417
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
3418
+ paymentDoneVC.bankPaymentParams = params
3419
+ // Pass billing and additional info
3420
+ // Conditionally pass raw FieldItem array
3421
+ paymentDoneVC.visibility = self.visibility
3422
+ paymentDoneVC.request = self.request
3423
+
3424
+ // if self.visibility?.billing == true {
3425
+ paymentDoneVC.billingInfoData = self.billingInfo
3426
+ var billingDict: [String: Any] = [:]
3427
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
3428
+ paymentDoneVC.billingInfo = billingDict
3429
+ // }
3430
+
3431
+ // if self.visibility?.additional == true {
3432
+ paymentDoneVC.additionalInfoData = self.additionalInfo
3433
+ var additionalDict: [String: Any] = [:]
3434
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
3435
+ paymentDoneVC.additionalInfo = additionalDict
3436
+ // }
3437
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
3438
+ }
3439
+ }
3440
+ }
3441
+ } else {
3442
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
3443
+ }
3444
+ } catch let jsonError {
3445
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
3446
+ }
3447
+ } else {
3448
+ self.presentPaymentErrorVC(errorMessage: "No data received")
3449
+ }
3450
+ } else {
3451
+ if let data = serviceData,
3452
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
3453
+ let message = responseObj["message"] as? String {
3454
+ self.presentPaymentErrorVC(errorMessage: message)
3455
+ } else {
3456
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
3457
+ }
3458
+ }
3459
+ }
3460
+ task.resume()
3461
+ }
3462
+
3463
+ }
3464
+
3465
+ //MARK: - Table View
3466
+ @available(iOS 16.0, *)
3467
+ extension BillingInfoVC: UITableViewDelegate, UITableViewDataSource {
3468
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
3469
+ if tableView == tblViewCountryList {
3470
+ // return countryList.count
3471
+ let count = isSearching ? filteredCountryList.count : countryList.count
3472
+ return count
3473
+ }
3474
+ else if tableView == tblViewStateList {
3475
+ // return stateList.count
3476
+ return isSearchingState ? filteredStateList.count : stateList.count
3477
+ }
3478
+ else if tableView == tblViewCityList {
3479
+ return cityList.count
3480
+ }
3481
+ else {
3482
+ return 0
3483
+ }
3484
+ }
3485
+
3486
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
3487
+ if tableView == tblViewCountryList {
3488
+ let currentList = isSearching ? filteredCountryList : countryList
3489
+ guard indexPath.row < currentList.count else {
3490
+ return UITableViewCell()
3491
+ }
3492
+
3493
+ let cell = tableView.dequeueReusableCell(withIdentifier: "CountryListTVC") as! CountryListTVC
3494
+ let country = currentList[indexPath.row]
3495
+ cell.lblCountryName.text = country["name"] as? String
3496
+ return cell
3497
+
3498
+ } else if tableView == tblViewStateList {
3499
+ // let cell = tableView.dequeueReusableCell(withIdentifier: "StateListTVC") as! StateListTVC
3500
+ // let state = stateList[indexPath.row]
3501
+ // cell.lblStateName.text = state["name"] as? String
3502
+ // return cell
3503
+
3504
+ let currentList = isSearchingState ? filteredStateList : stateList
3505
+ guard indexPath.row < currentList.count else { return UITableViewCell() }
3506
+
3507
+ let cell = tableView.dequeueReusableCell(withIdentifier: "StateListTVC") as! StateListTVC
3508
+ let state = currentList[indexPath.row]
3509
+ cell.lblStateName.text = state["name"] as? String
3510
+ return cell
3511
+ } else if tableView == tblViewCityList {
3512
+ let cell = tableView.dequeueReusableCell(withIdentifier: "CityListTVC") as! CityListTVC
3513
+ let city = cityList[indexPath.row]
3514
+ cell.lblCityName.text = city["city_name"] as? String
3515
+ return cell
3516
+ } else {
3517
+ return UITableViewCell()
3518
+ }
3519
+ }
3520
+
3521
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
3522
+ if tableView == tblViewCountryList {
3523
+
3524
+ let currentList = isSearching ? filteredCountryList : countryList
3525
+
3526
+ // Safety check to avoid index out of range
3527
+ guard indexPath.row < currentList.count else { return }
3528
+
3529
+ let selectedCountry = currentList[indexPath.row]["name"] as? String
3530
+
3531
+ txtFieldCountry.text = selectedCountry
3532
+ viewCountryList.isHidden = true
3533
+
3534
+ // Show/hide state dropdown and update placeholder
3535
+ if let country = selectedCountry {
3536
+ // if country.lowercased() == "united states" || country.lowercased() == "usa" || country.lowercased() == "canada" {
3537
+ // btnSelectState.isHidden = false
3538
+ // txtFieldState.placeholder = "Select State"
3539
+ // } else {
3540
+ // btnSelectState.isHidden = true
3541
+ // txtFieldState.placeholder = "State"
3542
+ // }
3543
+ //
3544
+ // getStateListApi(for: country)
3545
+
3546
+ handleCountrySelection(countryName: country)
3547
+ }
3548
+
3549
+
3550
+ // Reset search state after selection
3551
+ isSearching = false
3552
+ filteredCountryList.removeAll()
3553
+ searchBarCountryList.text = ""
3554
+ searchBarCountryList.resignFirstResponder()
3555
+
3556
+ tblViewCountryList.reloadData()
3557
+
3558
+ } else if tableView == tblViewStateList {
3559
+ // let selectedState = stateList[indexPath.row]["name"] as? String
3560
+ // txtFieldState.text = selectedState
3561
+ // viewStateList.isHidden = true
3562
+ //
3563
+ // // Fetch cities for the selected state and country
3564
+ // if let state = selectedState, let country = txtFieldCountry.text {
3565
+ // getCityListListApi(for: country, state: state)
3566
+ // }
3567
+
3568
+ let currentList = isSearchingState ? filteredStateList : stateList
3569
+ guard indexPath.row < currentList.count else { return }
3570
+
3571
+ let selectedState = currentList[indexPath.row]["name"] as? String
3572
+ txtFieldState.text = selectedState
3573
+ viewStateList.isHidden = true
3574
+
3575
+ isSearchingState = false
3576
+ filteredStateList.removeAll()
3577
+ searchBarStateList.text = ""
3578
+ searchBarStateList.resignFirstResponder()
3579
+
3580
+ if let state = selectedState, let country = txtFieldCountry.text {
3581
+ getCityListListApi(for: country, state: state)
3582
+ }
3583
+
3584
+ } else if tableView == tblViewCityList {
3585
+ let selectedCity = cityList[indexPath.row]["city_name"] as? String
3586
+ txtFieldCity.text = selectedCity
3587
+ viewCityList.isHidden = true
3588
+
3589
+ // Fetch the city list again based on selected state & country
3590
+ if let state = txtFieldState.text, let country = txtFieldCountry.text {
3591
+ getCityListListApi(for: country, state: state)
3592
+ }
3593
+ }
3594
+
3595
+ updateBillingInfoData()
3596
+ }
3597
+
3598
+ func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
3599
+ return 50
3600
+ }
3601
+
3602
+ }
3603
+
3604
+ extension BillingInfoVC: UISearchBarDelegate {
3605
+
3606
+ func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
3607
+ if searchBar == searchBarCountryList {
3608
+ if searchText.isEmpty {
3609
+ isSearching = false
3610
+ filteredCountryList.removeAll()
3611
+ view.endEditing(true)
3612
+ } else {
3613
+ isSearching = true
3614
+ filteredCountryList = countryList.filter {
3615
+ ($0["name"] as? String)?.lowercased().contains(searchText.lowercased()) ?? false
3616
+ }
3617
+ }
3618
+ tblViewCountryList.reloadData()
3619
+ } else if searchBar == searchBarStateList {
3620
+ if searchText.isEmpty {
3621
+ isSearchingState = false
3622
+ filteredStateList.removeAll()
3623
+ view.endEditing(true)
3624
+ } else {
3625
+ isSearchingState = true
3626
+ filteredStateList = stateList.filter {
3627
+ ($0["name"] as? String)?.lowercased().contains(searchText.lowercased()) ?? false
3628
+ }
3629
+ }
3630
+ tblViewStateList.reloadData()
3631
+ }
3632
+ }
3633
+
3634
+ func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
3635
+ if searchBar == searchBarCountryList {
3636
+ isSearching = true
3637
+ } else if searchBar == searchBarStateList {
3638
+ isSearchingState = true
3639
+ }
3640
+ }
3641
+
3642
+ func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
3643
+ if searchBar == searchBarCountryList {
3644
+ isSearching = false
3645
+ searchBar.text = ""
3646
+ searchBar.resignFirstResponder()
3647
+ tblViewCountryList.reloadData()
3648
+ } else if searchBar == searchBarStateList {
3649
+ isSearchingState = false
3650
+ searchBar.text = ""
3651
+ searchBar.resignFirstResponder()
3652
+ tblViewStateList.reloadData()
3653
+ }
3654
+ }
3655
+
3656
+ }
3657
+
3658
+ @available(iOS 16.0, *)
3659
+ extension BillingInfoVC: UITextFieldDelegate {
3660
+ func textFieldShouldReturn(_ textField: UITextField) -> Bool {
3661
+ textField.resignFirstResponder() // Dismiss the keyboard
3662
+ return true
3663
+ }
3664
+
3665
+ // Update billingInfoData when user finishes editing a field
3666
+ func textFieldDidEndEditing(_ textField: UITextField) {
3667
+ switch textField {
3668
+ case txtFieldAddress:
3669
+ setFieldValue("address", to: textField.text)
3670
+ case txtFieldCountry:
3671
+ setFieldValue("country", to: textField.text)
3672
+ case txtFieldState:
3673
+ setFieldValue("state", to: textField.text)
3674
+ case txtFieldCity:
3675
+ setFieldValue("city", to: textField.text)
3676
+ case txtFieldPostalCode:
3677
+ setFieldValue("postal_code", to: textField.text)
3678
+ default:
3679
+ break
3680
+ }
3681
+
3682
+ billingInfo = fieldSection?.billing
3683
+ }
3684
+
3685
+ }
3686
+