@jimrising/easymerchantsdk-react-native 2.2.1 → 2.2.3

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