@jimrising/easymerchantsdk-react-native 2.5.1 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/ios/Pods/Storyboard/EasyPaySdk.storyboard +9089 -0
  3. package/ios/Pods/UserDefaults/UserStoreSingleton.swift +424 -0
  4. package/ios/Pods/ViewControllers/AdditionalInfoVC.swift +2894 -0
  5. package/ios/Pods/ViewControllers/BaseVC.swift +142 -0
  6. package/ios/Pods/ViewControllers/BillingInfoVC/BillingInfoVC.swift +3686 -0
  7. package/ios/Pods/ViewControllers/BillingInfoVC/Cells/CityListTVC.swift +46 -0
  8. package/ios/Pods/ViewControllers/BillingInfoVC/Cells/CountryListTVC.swift +47 -0
  9. package/ios/Pods/ViewControllers/BillingInfoVC/Cells/StateListTVC.swift +46 -0
  10. package/ios/Pods/ViewControllers/Clean Runner_2025-07-23T14-58-05.txt +13 -0
  11. package/ios/Pods/ViewControllers/CountryListVC.swift +435 -0
  12. package/ios/Pods/ViewControllers/EmailVerificationVC.swift +286 -0
  13. package/ios/Pods/ViewControllers/GrailPayVC.swift +483 -0
  14. package/ios/Pods/ViewControllers/OTPVerificationVC.swift +2193 -0
  15. package/ios/Pods/ViewControllers/PaymentDoneVC.swift +284 -0
  16. package/ios/Pods/ViewControllers/PaymentErrorVC.swift +85 -0
  17. package/ios/Pods/ViewControllers/PaymentInformation/AccountTypeTVC.swift +41 -0
  18. package/ios/Pods/ViewControllers/PaymentInformation/PaymentInfoVC.swift +12875 -0
  19. package/ios/Pods/ViewControllers/PaymentInformation/PaymentInformationCVC.swift +35 -0
  20. package/ios/Pods/ViewControllers/PaymentInformation/RecurringTVC.swift +40 -0
  21. package/ios/Pods/ViewControllers/PaymentInformation/SavedAccountsTVC/SavedAccountTVC.swift +80 -0
  22. package/ios/Pods/ViewControllers/PaymentInformation/SavedAccountsTVC/SavedAccountTVC.xib +163 -0
  23. package/ios/Pods/ViewControllers/PaymentInformation/SavedCardsTVC/SavedCardsTVC.swift +81 -0
  24. package/ios/Pods/ViewControllers/PaymentInformation/SavedCardsTVC/SavedCardsTVC.xib +188 -0
  25. package/ios/Pods/ViewControllers/PaymentStatusWebViewVC.swift +158 -0
  26. package/ios/Pods/ViewControllers/TermAndConditionsVC.swift +63 -0
  27. package/ios/Pods/ViewControllers/ThreeDSecurePaymentDoneVC.swift +1216 -0
  28. package/ios/easymerchantsdk.podspec +1 -1
  29. package/package.json +1 -1
@@ -0,0 +1,2894 @@
1
+ //
2
+ // AdditionalInfoVC.swift
3
+ // EasyPay
4
+ //
5
+ // Created by Mony's Mac on 13/08/24.
6
+ //
7
+
8
+ import UIKit
9
+
10
+ @available(iOS 16.0, *)
11
+ class AdditionalInfoVC: BaseVC {
12
+
13
+ // @IBOutlet weak var viewAdditionalInfo: UIView!
14
+ @IBOutlet weak var txtFieldName: UITextField!
15
+ @IBOutlet weak var txtFieldEmail: UITextField!
16
+ @IBOutlet weak var txtFieldPhoneNumber: UITextField!
17
+ @IBOutlet weak var txtFieldDescription: UITextField!
18
+ @IBOutlet weak var lblCountryCode: UILabel!
19
+ @IBOutlet weak var btnPrevious: UIButton!
20
+ @IBOutlet weak var imgViewCountryFlag: UIImageView!
21
+
22
+ @IBOutlet weak var btnPayNow: UIButton!
23
+
24
+ @IBOutlet weak var lblAdditionalInfo: UILabel!
25
+ @IBOutlet weak var lblEasyMerchant: UILabel!
26
+ @IBOutlet weak var btnCountryCode: UIButton!
27
+
28
+ @IBOutlet weak var lblStarNameField: UILabel!
29
+ @IBOutlet weak var lblStarEmailField: UILabel!
30
+ @IBOutlet weak var lblStarPhoneField: UILabel!
31
+ @IBOutlet weak var lblStarDescriptionField: UILabel!
32
+
33
+ var cardNumber: String?
34
+ var expiryDate: String?
35
+ var cvv: String?
36
+ var nameOnCard: String?
37
+ var userEmail: String?
38
+
39
+ var selectedPaymentMethod: String?
40
+
41
+ //Banking Params
42
+ var accountName: String?
43
+ var routingNumber: String?
44
+ var accountType: String?
45
+ var accountNumber: String?
46
+
47
+ var easyPayDelegate: EasyPayViewControllerDelegate?
48
+
49
+ var isSavedForFuture: Bool = false
50
+
51
+ var selectedCard: CardModel?
52
+ // var amount: Int?
53
+ var amount: Double?
54
+ var cvvText: String?
55
+ var isFrom = String()
56
+ var isFromm = String()
57
+
58
+ var isSavedNewCard: Bool = false
59
+
60
+ //From Regular Saved Bank Accounts
61
+ var customerID: String?
62
+ var accountID: String?
63
+
64
+ var isSavedNewAccount: Bool?
65
+
66
+ var request: Request!
67
+
68
+ var chosenPlan: String?
69
+ var startDate: String?
70
+
71
+ //GrailPay Params
72
+ var grailPayAccountID: String?
73
+ var selectedGrailPayAccountType: String?
74
+ var selectedGrailPayAccountName: String?
75
+
76
+ var billingInfoData: Data?
77
+ var fieldSection: FieldSection?
78
+
79
+ var additionalInfo: [FieldItem]?
80
+ var billingInfo: [FieldItem]?
81
+ var visibility: FieldsVisibility?
82
+
83
+ override func viewDidLoad() {
84
+ super.viewDidLoad()
85
+ self.lblEasyMerchant.text = "POWERED BY \(UserStoreSingleton.shared.companyName?.uppercased() ?? "")"
86
+
87
+ let amountValue = Double(amount ?? 0)
88
+ let amountText = String(format: "$%.2f", amountValue)
89
+ let submitText = request?.submitButtonText ?? ""
90
+
91
+ let defaultTitle = !submitText.isEmpty
92
+ ? "\(submitText) (\(amountText))"
93
+ : "Pay Now (\(amountText))"
94
+
95
+ DispatchQueue.main.async {
96
+ self.btnPayNow.setTitle(defaultTitle, for: .normal)
97
+ }
98
+
99
+ uiFinishingTouchElements()
100
+
101
+ txtFieldName.delegate = self
102
+ txtFieldEmail.delegate = self
103
+ txtFieldDescription.delegate = self
104
+ txtFieldPhoneNumber.delegate = self
105
+
106
+ // Add tap gesture to hide the views and dismiss the keyboard
107
+ let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapOutside))
108
+ tapGesture.cancelsTouchesInView = false
109
+ self.view.addGestureRecognizer(tapGesture)
110
+
111
+ // Decode fieldSection if it's not already set
112
+ if fieldSection == nil, let billingInfoData = billingInfoData {
113
+ do {
114
+ let decodedFieldSection = try JSONDecoder().decode(FieldSection.self, from: billingInfoData)
115
+ self.fieldSection = decodedFieldSection
116
+ self.additionalInfo = decodedFieldSection.additional
117
+ self.billingInfo = decodedFieldSection.billing
118
+ self.visibility = decodedFieldSection.visibility
119
+ } catch {
120
+ }
121
+ }
122
+
123
+ if let additionalInfo = additionalInfo {
124
+ for item in additionalInfo {
125
+ switch item.name {
126
+ case "name":
127
+ txtFieldName.text = item.value
128
+ case "email":
129
+ txtFieldEmail.text = item.value
130
+ case "phone_number":
131
+ txtFieldPhoneNumber.text = item.value
132
+ case "description":
133
+ txtFieldDescription.text = item.value
134
+ default:
135
+ break
136
+ }
137
+ }
138
+ configureFieldVisibility()
139
+ } else if let billingInfoData = billingInfoData {
140
+ do {
141
+ let decodedFieldSection = try JSONDecoder().decode(FieldSection.self, from: billingInfoData)
142
+ self.fieldSection = decodedFieldSection
143
+
144
+ for item in decodedFieldSection.additional {
145
+ switch item.name {
146
+ case "name":
147
+ txtFieldName.text = item.value
148
+ case "email":
149
+ txtFieldEmail.text = item.value
150
+ case "phone_number":
151
+ txtFieldPhoneNumber.text = item.value
152
+ case "description":
153
+ txtFieldDescription.text = item.value
154
+ default:
155
+ break
156
+ }
157
+ }
158
+ configureFieldVisibility()
159
+ } catch {
160
+ }
161
+ }
162
+
163
+ if let flag = String.flag(for: "US") {
164
+ lblCountryCode.text = "\(flag) +1"
165
+ }
166
+
167
+ //Show Default USA code in starting.
168
+ let usaCountry = Country()
169
+ usaCountry.name = "United States"
170
+ usaCountry.countryCode = "US"
171
+ usaCountry.extensionCode = "1"
172
+ usaCountry.flag = String.flag(for: "US")
173
+ lblCountryCode.text = "\(usaCountry.flag ?? "") +\(usaCountry.extensionCode ?? "")"
174
+ }
175
+
176
+ override func viewWillAppear(_ animated: Bool) {
177
+ uiFinishingTouchElements()
178
+ configureFieldVisibility()
179
+ }
180
+
181
+ func uiFinishingTouchElements() {
182
+ // Set background color for the main view
183
+ if let containerBGcolor = UserStoreSingleton.shared.container_bg_col,
184
+ let uiColor = UIColor(hex: containerBGcolor) {
185
+ self.view.backgroundColor = uiColor
186
+ }
187
+
188
+ if let primaryBtnBackGroundColor = UserStoreSingleton.shared.primary_btn_bg_col,
189
+ let uiColor = UIColor(hex: primaryBtnBackGroundColor) {
190
+ btnPayNow.backgroundColor = uiColor
191
+ btnPrevious.setTitleColor(uiColor, for: .normal)
192
+ btnPrevious.layer.borderColor = uiColor.cgColor
193
+ }
194
+
195
+ if let primaryBtnFontColor = UserStoreSingleton.shared.primary_btn_font_col,
196
+ let secondaryUIColor = UIColor(hex: primaryBtnFontColor) {
197
+ btnPayNow.setTitleColor(secondaryUIColor, for: .normal)
198
+ }
199
+
200
+ if let secondaryFontColor = UserStoreSingleton.shared.secondary_font_col,
201
+ let placeholderColor = UIColor(hex: secondaryFontColor) {
202
+ lblEasyMerchant.textColor = placeholderColor
203
+ }
204
+
205
+ if let borderRadiusString = UserStoreSingleton.shared.border_radious,
206
+ let borderRadius = Double(borderRadiusString) { // Convert String to Double
207
+ btnPayNow.layer.cornerRadius = CGFloat(borderRadius) // Set corner radius
208
+ btnPrevious.layer.cornerRadius = CGFloat(borderRadius)
209
+ btnPrevious.layer.borderWidth = 1
210
+ } else {
211
+ btnPayNow.layer.cornerRadius = 8 // Default value
212
+ btnPrevious.layer.cornerRadius = 8
213
+ }
214
+ btnPayNow.layer.masksToBounds = true // Ensure the corners are clipped properly
215
+ btnPrevious.layer.masksToBounds = true
216
+
217
+ if let primaryFontColor = UserStoreSingleton.shared.primary_font_col,
218
+ let uiColor = UIColor(hex: primaryFontColor) {
219
+ lblAdditionalInfo.textColor = uiColor
220
+ lblCountryCode.textColor = uiColor
221
+ }
222
+
223
+ if let fontSizeString = UserStoreSingleton.shared.fontSize,
224
+ let fontSizeDouble = Double(fontSizeString) { // Convert String to Double
225
+ let fontSize = CGFloat(fontSizeDouble) // Convert Double to CGFloat
226
+ lblEasyMerchant.font = UIFont.systemFont(ofSize: fontSize)
227
+ lblCountryCode.font = UIFont.systemFont(ofSize: fontSize)
228
+ btnPayNow.titleLabel?.font = UIFont.systemFont(ofSize: fontSize)
229
+ btnPrevious.titleLabel?.font = UIFont.systemFont(ofSize: fontSize)
230
+ }
231
+
232
+ }
233
+
234
+ private func getFieldValue(for name: String) -> String? {
235
+ return fieldSection?.additional.first(where: { $0.name == name })?.value
236
+ }
237
+
238
+ private func setFieldValue(_ name: String, to value: String?) {
239
+ guard let index = fieldSection?.additional.firstIndex(where: { $0.name == name }) else { return }
240
+ fieldSection?.additional[index].value = value ?? ""
241
+ }
242
+
243
+ private func configureFieldVisibility() {
244
+ guard let additionalFields = fieldSection?.additional else {
245
+ return
246
+ }
247
+
248
+ func isFieldRequired(_ name: String) -> Bool {
249
+ let required = additionalFields.first(where: { $0.name == name })?.required == true
250
+ return required
251
+ }
252
+
253
+ DispatchQueue.main.async {
254
+ self.lblStarNameField.isHidden = !isFieldRequired("name")
255
+ self.lblStarEmailField.isHidden = !isFieldRequired("email_address")
256
+ self.lblStarPhoneField.isHidden = !isFieldRequired("phone_number")
257
+ self.lblStarDescriptionField.isHidden = !isFieldRequired("description")
258
+ self.view.layoutIfNeeded()
259
+ }
260
+ }
261
+
262
+ @objc func didTapOutside() {
263
+ // Dismiss the keyboard
264
+ self.view.endEditing(true)
265
+ }
266
+
267
+ @IBAction func actionBtnSelectCountryCode(_ sender: UIButton) {
268
+ let vc = EasyPaySdk.instantiateViewController(withIdentifier: "CountryListVC") as! CountryListVC
269
+ vc.delegate = self
270
+ self.navigationController?.pushViewController(vc, animated: true)
271
+ }
272
+
273
+ @IBAction func actionBtnPrevious(_ sender: UIButton) {
274
+ navigationController?.popViewController(animated: true)
275
+ }
276
+
277
+ func updateAdditionalInfoData() {
278
+ setAdditionalFieldValue("name", to: txtFieldName.text)
279
+ setAdditionalFieldValue("email", to: txtFieldEmail.text)
280
+ setAdditionalFieldValue("phone_number", to: txtFieldPhoneNumber.text)
281
+ setAdditionalFieldValue("description", to: txtFieldDescription.text)
282
+ additionalInfo = fieldSection?.additional
283
+ }
284
+
285
+ // Helper method
286
+ func setAdditionalFieldValue(_ name: String, to value: String?) {
287
+ if let index = fieldSection?.additional.firstIndex(where: { $0.name == name }) {
288
+ fieldSection?.additional[index].value = value ?? ""
289
+ }
290
+ }
291
+
292
+ @IBAction func actionBtnPayNow(_ sender: UIButton) {
293
+ // MARK: - Validation
294
+ if !lblStarPhoneField.isHidden &&
295
+ txtFieldPhoneNumber.text?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == true {
296
+ showAlert(title: "Missing Information", message: "Please enter your phone number.")
297
+ return
298
+ } else if !lblStarDescriptionField.isHidden &&
299
+ txtFieldDescription.text?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == true {
300
+ showAlert(title: "Missing Information", message: "Please enter description text.")
301
+ return
302
+ }
303
+
304
+ // MARK: - Update Additional Info
305
+ updateAdditionalInfoData()
306
+
307
+ // MARK: - Encode Updated fieldSection to billingInfoData
308
+ if let updatedFieldSection = fieldSection,
309
+ let updatedBillingData = try? JSONEncoder().encode(updatedFieldSection) {
310
+ billingInfoData = updatedBillingData
311
+ } else {
312
+ return
313
+ }
314
+
315
+ // MARK: - Flow Based on Conditions
316
+ if isSavedForFuture {
317
+ // ✅ If logged in → Call API directly, no navigation
318
+ if UserStoreSingleton.shared.isLoggedIn == true {
319
+ switch selectedPaymentMethod {
320
+ case "Card":
321
+ if request.secureAuthentication == true {
322
+ threeDSecurePaymentApi()
323
+ } else {
324
+ paymentIntentApi()
325
+ }
326
+
327
+ case "Bank":
328
+ accountChargeApi()
329
+
330
+ case "GrailPay":
331
+ grailPayAccountChargeApi()
332
+
333
+ case "NewGrailPayAccount":
334
+ grailPayAccountChargeApi(customerId: UserStoreSingleton.shared.customerId)
335
+
336
+ default:
337
+ }
338
+ return
339
+ }
340
+
341
+ // ❌ Not logged in → Always navigate to OTPVerificationVC
342
+ if let emailVerificationVC = self.storyboard?.instantiateViewController(withIdentifier: "OTPVerificationVC") as? OTPVerificationVC {
343
+
344
+ emailVerificationVC.billingInfoData = billingInfoData
345
+ emailVerificationVC.selectedPaymentMethod = selectedPaymentMethod
346
+ emailVerificationVC.easyPayDelegate = easyPayDelegate
347
+ emailVerificationVC.request = request
348
+ emailVerificationVC.chosenPlan = chosenPlan
349
+ emailVerificationVC.startDate = startDate
350
+ emailVerificationVC.userEmail = userEmail
351
+ emailVerificationVC.billingInfo = fieldSection?.billing
352
+ emailVerificationVC.additionalInfo = fieldSection?.additional
353
+ emailVerificationVC.visibility = fieldSection?.visibility
354
+ emailVerificationVC.amount = amount
355
+ emailVerificationVC.email = userEmail
356
+
357
+ // Payment method-specific data
358
+ switch selectedPaymentMethod {
359
+ case "Card":
360
+ emailVerificationVC.cardNumber = cardNumber
361
+ emailVerificationVC.expiryDate = expiryDate
362
+ emailVerificationVC.cvv = cvv
363
+ emailVerificationVC.nameOnCard = nameOnCard
364
+
365
+ case "Bank":
366
+ emailVerificationVC.accountName = accountName
367
+ emailVerificationVC.routingNumber = routingNumber
368
+ emailVerificationVC.accountType = accountType
369
+ emailVerificationVC.accountNumber = accountNumber
370
+
371
+ case "GrailPay":
372
+ emailVerificationVC.grailPayAccountID = grailPayAccountID
373
+ emailVerificationVC.selectedGrailPayAccountType = selectedGrailPayAccountType
374
+ emailVerificationVC.selectedGrailPayAccountName = selectedGrailPayAccountName
375
+ emailVerificationVC.isSavedForFuture = true
376
+
377
+ default:
378
+ break
379
+ }
380
+
381
+ navigationController?.pushViewController(emailVerificationVC, animated: true)
382
+ }
383
+ }
384
+
385
+ else {
386
+ // Direct Payment Flow
387
+ if selectedPaymentMethod == "Card" {
388
+ if isFrom == "SavedCards" {
389
+ paymentIntentFromShowCardApi()
390
+ }
391
+ else if isSavedNewCard {
392
+ if isFrom == "AddNewCard" {
393
+ if request.secureAuthentication == true {
394
+ threeDSecurePaymentAddNewCardApi(customerId: UserStoreSingleton.shared.customerId)
395
+ } else {
396
+ paymentIntentAddNewCardApi(customerId: UserStoreSingleton.shared.customerId)
397
+ }
398
+ }
399
+ }
400
+ else {
401
+ if request.secureAuthentication == true {
402
+ threeDSecurePaymentApi()
403
+ } else {
404
+ paymentIntentApi()
405
+ }
406
+ }
407
+ }
408
+ else if selectedPaymentMethod == "Bank" {
409
+ if isFrom == "SavedBank" {
410
+ accountChargeSavedBankAccountApi()
411
+ }
412
+ else if isFrom == "NormalBankPayWithoutSave" {
413
+ accountChargeApi()
414
+ }
415
+ else if isFrom == "AddNewAccountWithoutSave" {
416
+ accountChargeApi()
417
+ }
418
+ else if isFrom == "AddNewAccountWithSave" {
419
+ accountChargeApi(customerId: UserStoreSingleton.shared.customerId)
420
+ }
421
+ }
422
+ else if selectedPaymentMethod == "GrailPay" {
423
+ grailPayAccountChargeApi()
424
+ }
425
+ else if selectedPaymentMethod == "NewGrailPayAccount" {
426
+ grailPayAccountChargeApi()
427
+ }
428
+ }
429
+ }
430
+
431
+ func presentPaymentErrorVC(errorMessage: String) {
432
+ DispatchQueue.main.async {
433
+ if let paymentErrorVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentErrorVC") as? PaymentErrorVC {
434
+ paymentErrorVC.errorMessage = errorMessage
435
+ paymentErrorVC.easyPayDelegate = self.easyPayDelegate // Pass the reference here
436
+ self.navigationController?.pushViewController(paymentErrorVC, animated: true)
437
+ }
438
+ }
439
+ }
440
+
441
+ // MARK: - Credit Card Charge Api
442
+ func paymentIntentApi() {
443
+ showLoadingIndicator()
444
+
445
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.charges.path()
446
+
447
+ guard let serviceURL = URL(string: fullURL) else {
448
+ hideLoadingIndicator()
449
+ return
450
+ }
451
+
452
+ var urlRequest = URLRequest(url: serviceURL)
453
+ urlRequest.httpMethod = "POST"
454
+ urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
455
+
456
+ let token = UserStoreSingleton.shared.clientToken
457
+ urlRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
458
+
459
+ // Extract only the digits from the phone number (local only, no country code)
460
+ let localPhone = txtFieldPhoneNumber.text?.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() ?? ""
461
+
462
+ var params: [String: Any] = [
463
+ "name": nameOnCard ?? "",
464
+ "email": userEmail ?? "",
465
+ "card_number": cardNumber?.replacingOccurrences(of: " ", with: "") ?? "",
466
+ "cardholder_name": nameOnCard ?? "",
467
+ "exp_month": expiryDate?.components(separatedBy: "/").first ?? "",
468
+ "exp_year": expiryDate?.components(separatedBy: "/").last ?? "",
469
+ "cvc": cvv ?? "",
470
+ "currency": "usd"
471
+ ]
472
+
473
+ // ✅ Only for logged-in users
474
+ if UserStoreSingleton.shared.isLoggedIn == true {
475
+ let emailText = userEmail
476
+ let emailPrefix = emailText?.components(separatedBy: "@").first ?? ""
477
+
478
+ params["save_card"] = isSavedNewCard ? 1 : 0
479
+ if isSavedNewCard {
480
+ params["is_default"] = "1"
481
+ }
482
+ params["tokenize"] = request.tokenOnly ?? ""
483
+ params["username"] = emailPrefix
484
+
485
+ if let customerId = UserStoreSingleton.shared.customerId {
486
+ params["customer"] = customerId
487
+ params["customer_id"] = customerId
488
+ } else {
489
+ params["create_customer"] = "1"
490
+ }
491
+
492
+ if UserStoreSingleton.shared.customerId == nil {
493
+ params["create_customer"] = "1"
494
+ }
495
+ }
496
+
497
+ // Conditionally add billing info
498
+ if let visibility = visibility, visibility.billing == true,
499
+ let billing = billingInfo, !billing.isEmpty {
500
+
501
+ var billingInfoDict: [String: Any] = [:]
502
+ for item in billing {
503
+ billingInfoDict[item.name] = item.value
504
+ }
505
+
506
+ params["address"] = billingInfoDict["address"] as? String ?? ""
507
+ params["country"] = billingInfoDict["country"] as? String ?? ""
508
+ params["state"] = billingInfoDict["state"] as? String ?? ""
509
+ params["city"] = billingInfoDict["city"] as? String ?? ""
510
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
511
+ }
512
+
513
+ // Conditionally add additional info
514
+ if let visibility = visibility, visibility.additional == true,
515
+ let additional = additionalInfo, !additional.isEmpty {
516
+ params["description"] = txtFieldDescription.text ?? ""
517
+ params["phone_number"] = localPhone
518
+ }
519
+
520
+ // Add these if recurring is enabled
521
+ // if let req = request, req.is_recurring == true {
522
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
523
+ // // Only send start_date if type is .custom and field is not empty
524
+ // if let startDateText = startDate, !startDateText.isEmpty {
525
+ // let inputFormatter = DateFormatter()
526
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
527
+ //
528
+ // let outputFormatter = DateFormatter()
529
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
530
+ //
531
+ // if let date = inputFormatter.date(from: startDateText) {
532
+ // let apiFormattedDate = outputFormatter.string(from: date)
533
+ // params["start_date"] = apiFormattedDate
534
+ // } else {
535
+ // }
536
+ // }
537
+ // }
538
+ //
539
+ // params["interval"] = chosenPlan?.lowercased()
540
+ // }
541
+
542
+ // Add these if recurring is enabled
543
+ if let req = request, req.is_recurring == true {
544
+ if let startDateText = startDate, !startDateText.isEmpty {
545
+ let inputFormatter = DateFormatter()
546
+ inputFormatter.dateFormat = "dd/MM/yyyy"
547
+
548
+ let outputFormatter = DateFormatter()
549
+ outputFormatter.dateFormat = "MM/dd/yyyy"
550
+
551
+ if let date = inputFormatter.date(from: startDateText) {
552
+ let apiFormattedDate = outputFormatter.string(from: date)
553
+ params["start_date"] = apiFormattedDate
554
+ } else {
555
+ }
556
+ }
557
+
558
+ // interval is still required
559
+ params["interval"] = chosenPlan?.lowercased()
560
+ }
561
+
562
+ // ✅ Include metadata only if it has at least 1 key-value pair
563
+ if let metadata = request?.metadata, !metadata.isEmpty {
564
+ params["metadata"] = metadata
565
+ }
566
+
567
+
568
+ do {
569
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
570
+ urlRequest.httpBody = jsonData
571
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
572
+ }
573
+ } catch let error {
574
+ hideLoadingIndicator()
575
+ return
576
+ }
577
+
578
+ let session = URLSession.shared
579
+ let task = session.dataTask(with: urlRequest) { (serviceData, serviceResponse, error) in
580
+
581
+ DispatchQueue.main.async {
582
+ self.hideLoadingIndicator()
583
+ }
584
+
585
+ if let error = error {
586
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
587
+ return
588
+ }
589
+
590
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
591
+ self.presentPaymentErrorVC(errorMessage: "Invalid response from server.")
592
+ return
593
+ }
594
+
595
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
596
+ if let data = serviceData {
597
+ do {
598
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
599
+
600
+ // ✅ Handle duplicate transaction case
601
+ if let status = responseObject["status"] as? Bool, status == false,
602
+ let message = responseObject["message"] as? String,
603
+ message.lowercased().contains("duplicate transaction") {
604
+ self.presentPaymentErrorVC(errorMessage: message)
605
+ return
606
+ }
607
+
608
+ if let status = responseObject["status"] as? Int, status == 0,
609
+ let message = responseObject["message"] as? String,
610
+ message.lowercased().contains("duplicate transaction") {
611
+ self.presentPaymentErrorVC(errorMessage: message)
612
+ return
613
+ }
614
+
615
+ // ✅ Handle generic "status == 0" error case
616
+ if let status = responseObject["status"] as? Int, status == 0 {
617
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
618
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
619
+ return
620
+ }
621
+ else {
622
+ DispatchQueue.main.async {
623
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
624
+ paymentDoneVC.chargeData = responseObject
625
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
626
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
627
+ // Pass billing and additional info
628
+ // Conditionally pass raw FieldItem array
629
+ paymentDoneVC.visibility = self.visibility
630
+ paymentDoneVC.request = self.request
631
+
632
+ // if self.visibility?.billing == true {
633
+ paymentDoneVC.billingInfoData = self.billingInfo
634
+ var billingDict: [String: Any] = [:]
635
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
636
+ paymentDoneVC.billingInfo = billingDict
637
+ // }
638
+
639
+ // if self.visibility?.additional == true {
640
+ // Update additionalInfo values before sending
641
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "description" }) {
642
+ self.additionalInfo?[index].value = self.txtFieldDescription.text ?? ""
643
+ }
644
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "phone_number" }) {
645
+ self.additionalInfo?[index].value = localPhone
646
+ }
647
+
648
+ paymentDoneVC.additionalInfoData = self.additionalInfo
649
+
650
+ var additionalDict: [String: Any] = [:]
651
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
652
+ paymentDoneVC.additionalInfo = additionalDict
653
+ // }
654
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
655
+ }
656
+ }
657
+ }
658
+ } else {
659
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
660
+ }
661
+ } catch let jsonError {
662
+ self.presentPaymentErrorVC(errorMessage: "Error parsing response: \(jsonError.localizedDescription)")
663
+ }
664
+ } else {
665
+ self.presentPaymentErrorVC(errorMessage: "No data received from server.")
666
+ }
667
+ } else {
668
+ if let data = serviceData,
669
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
670
+ let message = responseObj["message"] as? String {
671
+ self.presentPaymentErrorVC(errorMessage: message)
672
+ } else {
673
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
674
+ }
675
+ }
676
+ }
677
+ task.resume()
678
+ }
679
+
680
+ //MARK: - Credit Card Charge Api from Saved cards
681
+ func paymentIntentFromShowCardApi() {
682
+
683
+ Thread.callStackSymbols.forEach { print($0) }
684
+
685
+ showLoadingIndicator()
686
+
687
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.charges.path()
688
+
689
+ guard let serviceURL = URL(string: fullURL) else {
690
+ hideLoadingIndicator()
691
+ return
692
+ }
693
+
694
+ var uRLRequest = URLRequest(url: serviceURL)
695
+ uRLRequest.httpMethod = "POST"
696
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
697
+
698
+ let token = UserStoreSingleton.shared.clientToken
699
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
700
+
701
+ // Extract only the digits from the phone number (local only, no country code)
702
+ let localPhone = txtFieldPhoneNumber.text?.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() ?? ""
703
+
704
+ // let emailText = UserStoreSingleton.shared.verificationEmail ?? ""
705
+ // let emailPrefix = emailText.components(separatedBy: "@").first ?? ""
706
+
707
+ let finalEmail: String
708
+ if let verificationEmail = UserStoreSingleton.shared.verificationEmail, !verificationEmail.isEmpty {
709
+ finalEmail = verificationEmail
710
+ } else {
711
+ finalEmail = request.email ?? ""
712
+ }
713
+
714
+ let emailPrefix: String
715
+ if !finalEmail.isEmpty {
716
+ emailPrefix = finalEmail.components(separatedBy: "@").first ?? ""
717
+ } else {
718
+ emailPrefix = ""
719
+ }
720
+
721
+ // Determine name: use request.name if available, otherwise fallback to email prefix
722
+ let nameParam: String
723
+ if let requestName = request.name, !requestName.trimmingCharacters(in: .whitespaces).isEmpty {
724
+ nameParam = requestName
725
+ } else {
726
+ nameParam = emailPrefix
727
+ }
728
+
729
+ var params: [String: Any] = [
730
+ "description": txtFieldDescription.text ?? "",
731
+ "currency": "usd",
732
+ "payment_method": "card",
733
+ "save_card": 0,
734
+ "customer" : selectedCard?.customerId ?? "",
735
+ "customer_id" : selectedCard?.customerId ?? "",
736
+ "card_id" : selectedCard?.cardId ?? "",
737
+ "cvc" : cvvText ?? "",
738
+ "name": nameParam,
739
+ "email": finalEmail
740
+ ]
741
+
742
+ // Conditionally add billing info
743
+ if let visibility = visibility, visibility.billing == true,
744
+ let billing = billingInfo, !billing.isEmpty {
745
+
746
+ var billingInfoDict: [String: Any] = [:]
747
+ for item in billing {
748
+ billingInfoDict[item.name] = item.value
749
+ }
750
+
751
+ params["address"] = billingInfoDict["address"] as? String ?? ""
752
+ params["country"] = billingInfoDict["country"] as? String ?? ""
753
+ params["state"] = billingInfoDict["state"] as? String ?? ""
754
+ params["city"] = billingInfoDict["city"] as? String ?? ""
755
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
756
+ }
757
+
758
+ // Default values
759
+ let defaultDescription = "Hosted payment checkout"
760
+ // Additional Info
761
+ if let visibility = visibility, visibility.additional == true,
762
+ let additional = additionalInfo, !additional.isEmpty {
763
+
764
+ var additionalInfoDict: [String: Any] = [:]
765
+ for item in additional {
766
+ additionalInfoDict[item.name] = item.value
767
+ }
768
+
769
+ // Description
770
+ let description = (additionalInfoDict["description"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
771
+ params["description"] = (description?.isEmpty == false) ? description! : defaultDescription
772
+
773
+ // Phone
774
+ params["phone_number"] = localPhone
775
+ } else {
776
+ // Fallback if additional section not visible
777
+ params["description"] = defaultDescription
778
+ }
779
+
780
+ // Add these if recurring is enabled
781
+ // if let req = request, req.is_recurring == true {
782
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
783
+ // // Only send start_date if type is .custom and field is not empty
784
+ // if let startDateText = startDate, !startDateText.isEmpty {
785
+ // let inputFormatter = DateFormatter()
786
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
787
+ //
788
+ // let outputFormatter = DateFormatter()
789
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
790
+ //
791
+ // if let date = inputFormatter.date(from: startDateText) {
792
+ // let apiFormattedDate = outputFormatter.string(from: date)
793
+ // params["start_date"] = apiFormattedDate
794
+ // } else {
795
+ // }
796
+ // }
797
+ // }
798
+ //
799
+ // params["interval"] = chosenPlan?.lowercased()
800
+ // }
801
+
802
+ // Add these if recurring is enabled
803
+ if let req = request, req.is_recurring == true {
804
+ if let startDateText = startDate, !startDateText.isEmpty {
805
+ let inputFormatter = DateFormatter()
806
+ inputFormatter.dateFormat = "dd/MM/yyyy"
807
+
808
+ let outputFormatter = DateFormatter()
809
+ outputFormatter.dateFormat = "MM/dd/yyyy"
810
+
811
+ if let date = inputFormatter.date(from: startDateText) {
812
+ let apiFormattedDate = outputFormatter.string(from: date)
813
+ params["start_date"] = apiFormattedDate
814
+ } else {
815
+ }
816
+ }
817
+
818
+ // interval is still required
819
+ params["interval"] = chosenPlan?.lowercased()
820
+ }
821
+
822
+ // ✅ Include metadata only if it has at least 1 key-value pair
823
+ if let metadata = request?.metadata, !metadata.isEmpty {
824
+ params["metadata"] = metadata
825
+ }
826
+
827
+
828
+ do {
829
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
830
+ uRLRequest.httpBody = jsonData
831
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
832
+ }
833
+ } catch let error {
834
+ hideLoadingIndicator()
835
+ return
836
+ }
837
+
838
+ let session = URLSession.shared
839
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
840
+
841
+ DispatchQueue.main.async {
842
+ self.hideLoadingIndicator() // Stop loader when response is received
843
+ }
844
+
845
+ if let error = error {
846
+ return
847
+ }
848
+
849
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
850
+ return
851
+ }
852
+
853
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
854
+ if let data = serviceData {
855
+ do {
856
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
857
+
858
+ // ✅ Handle duplicate transaction case
859
+ if let status = responseObject["status"] as? Bool, status == false,
860
+ let message = responseObject["message"] as? String,
861
+ message.lowercased().contains("duplicate transaction") {
862
+ self.presentPaymentErrorVC(errorMessage: message)
863
+ return
864
+ }
865
+
866
+ if let status = responseObject["status"] as? Int, status == 0,
867
+ let message = responseObject["message"] as? String,
868
+ message.lowercased().contains("duplicate transaction") {
869
+ self.presentPaymentErrorVC(errorMessage: message)
870
+ return
871
+ }
872
+
873
+ // ✅ Handle generic "status == 0" error case
874
+ if let status = responseObject["status"] as? Int, status == 0 {
875
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
876
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
877
+ return
878
+ }
879
+ else {
880
+ DispatchQueue.main.async {
881
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
882
+ paymentDoneVC.chargeData = responseObject
883
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
884
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
885
+ // Pass billing and additional info
886
+ // Conditionally pass raw FieldItem array
887
+ paymentDoneVC.visibility = self.visibility
888
+ paymentDoneVC.request = self.request
889
+
890
+ // if self.visibility?.billing == true {
891
+ paymentDoneVC.billingInfoData = self.billingInfo
892
+ var billingDict: [String: Any] = [:]
893
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
894
+ paymentDoneVC.billingInfo = billingDict
895
+ // }
896
+
897
+ // if self.visibility?.additional == true {
898
+ // Update additionalInfo values before sending
899
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "description" }) {
900
+ self.additionalInfo?[index].value = self.txtFieldDescription.text ?? ""
901
+ }
902
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "phone_number" }) {
903
+ self.additionalInfo?[index].value = localPhone
904
+ }
905
+
906
+ paymentDoneVC.additionalInfoData = self.additionalInfo
907
+
908
+ var additionalDict: [String: Any] = [:]
909
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
910
+ paymentDoneVC.additionalInfo = additionalDict
911
+ // }
912
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
913
+ }
914
+ }
915
+ }
916
+ } else {
917
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
918
+ }
919
+ } catch let jsonError {
920
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
921
+ }
922
+ } else {
923
+ self.presentPaymentErrorVC(errorMessage: "No data received")
924
+ }
925
+ } else {
926
+ if let data = serviceData,
927
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
928
+ let message = responseObj["message"] as? String {
929
+ self.presentPaymentErrorVC(errorMessage: message)
930
+ } else {
931
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
932
+ }
933
+ }
934
+ }
935
+ task.resume()
936
+ }
937
+
938
+ //MARK: - Credit Card Charge Api from Add new card from saved cards.
939
+ func paymentIntentAddNewCardApi(customerId: String?) {
940
+ showLoadingIndicator()
941
+
942
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.charges.path()
943
+
944
+ guard let serviceURL = URL(string: fullURL) else {
945
+ hideLoadingIndicator()
946
+ return
947
+ }
948
+
949
+ var uRLRequest = URLRequest(url: serviceURL)
950
+ uRLRequest.httpMethod = "POST"
951
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
952
+
953
+ let token = UserStoreSingleton.shared.clientToken
954
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
955
+
956
+ // Extract only the digits from the phone number (local only, no country code)
957
+ let localPhone = txtFieldPhoneNumber.text?.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() ?? ""
958
+
959
+ // let emailPrefix = UserStoreSingleton.shared.verificationEmail?.components(separatedBy: "@").first ?? ""
960
+
961
+ let finalEmail: String
962
+ if let verificationEmail = UserStoreSingleton.shared.verificationEmail, !verificationEmail.isEmpty {
963
+ finalEmail = verificationEmail
964
+ } else {
965
+ finalEmail = request.email ?? ""
966
+ }
967
+
968
+ let emailPrefix: String
969
+ if !finalEmail.isEmpty {
970
+ emailPrefix = finalEmail.components(separatedBy: "@").first ?? ""
971
+ } else {
972
+ emailPrefix = ""
973
+ }
974
+
975
+ var params: [String: Any] = [
976
+ "name": nameOnCard ?? "",
977
+ "email": finalEmail,
978
+ "card_number": cardNumber?.replacingOccurrences(of: " ", with: "") ?? "",
979
+ "cardholder_name": nameOnCard ?? "",
980
+ "exp_month": expiryDate?.components(separatedBy: "/").first ?? "",
981
+ "exp_year": expiryDate?.components(separatedBy: "/").last ?? "",
982
+ "cvc": cvv ?? "",
983
+ "currency": "usd",
984
+ "payment_method": selectedPaymentMethod ?? "",
985
+ "save_card": isSavedNewCard ? 1 : 0
986
+ ]
987
+
988
+ // Add is_default parameter if save_card is 1
989
+ if isSavedNewCard {
990
+ params["is_default"] = "1"
991
+ }
992
+
993
+ // Conditionally add billing info
994
+ if let visibility = visibility, visibility.billing == true,
995
+ let billing = billingInfo, !billing.isEmpty {
996
+
997
+ var billingInfoDict: [String: Any] = [:]
998
+ for item in billing {
999
+ billingInfoDict[item.name] = item.value
1000
+ }
1001
+
1002
+ params["address"] = billingInfoDict["address"] as? String ?? ""
1003
+ params["country"] = billingInfoDict["country"] as? String ?? ""
1004
+ params["state"] = billingInfoDict["state"] as? String ?? ""
1005
+ params["city"] = billingInfoDict["city"] as? String ?? ""
1006
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
1007
+ }
1008
+
1009
+ // Conditionally add additional info
1010
+ if let visibility = visibility, visibility.additional == true,
1011
+ let additional = additionalInfo, !additional.isEmpty {
1012
+ params["description"] = txtFieldDescription.text ?? ""
1013
+ params["phone_number"] = localPhone
1014
+ }
1015
+
1016
+ // Add these if recurring is enabled
1017
+ // if let req = request, req.is_recurring == true {
1018
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1019
+ // // Only send start_date if type is .custom and field is not empty
1020
+ // if let startDateText = startDate, !startDateText.isEmpty {
1021
+ // let inputFormatter = DateFormatter()
1022
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1023
+ //
1024
+ // let outputFormatter = DateFormatter()
1025
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
1026
+ //
1027
+ // if let date = inputFormatter.date(from: startDateText) {
1028
+ // let apiFormattedDate = outputFormatter.string(from: date)
1029
+ // params["start_date"] = apiFormattedDate
1030
+ // } else {
1031
+ // }
1032
+ // }
1033
+ // }
1034
+ //
1035
+ // params["interval"] = chosenPlan?.lowercased()
1036
+ // }
1037
+
1038
+ // Add these if recurring is enabled
1039
+ if let req = request, req.is_recurring == true {
1040
+ if let startDateText = startDate, !startDateText.isEmpty {
1041
+ let inputFormatter = DateFormatter()
1042
+ inputFormatter.dateFormat = "dd/MM/yyyy"
1043
+
1044
+ let outputFormatter = DateFormatter()
1045
+ outputFormatter.dateFormat = "MM/dd/yyyy"
1046
+
1047
+ if let date = inputFormatter.date(from: startDateText) {
1048
+ let apiFormattedDate = outputFormatter.string(from: date)
1049
+ params["start_date"] = apiFormattedDate
1050
+ } else {
1051
+ }
1052
+ }
1053
+
1054
+ // interval is still required
1055
+ params["interval"] = chosenPlan?.lowercased()
1056
+ }
1057
+
1058
+ if let customerId = customerId {
1059
+ params["customer"] = customerId
1060
+ params["customer_id"] = customerId
1061
+ } else {
1062
+ params["username"] = emailPrefix
1063
+ params["email"] = finalEmail
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
+
1072
+ do {
1073
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
1074
+ uRLRequest.httpBody = jsonData
1075
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1076
+ }
1077
+ } catch let error {
1078
+ hideLoadingIndicator()
1079
+ return
1080
+ }
1081
+
1082
+ let session = URLSession.shared
1083
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
1084
+
1085
+ DispatchQueue.main.async {
1086
+ self.hideLoadingIndicator() // Stop loader when response is received
1087
+ }
1088
+
1089
+ if let error = error {
1090
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
1091
+ return
1092
+ }
1093
+
1094
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
1095
+ self.presentPaymentErrorVC(errorMessage: "Invalid response")
1096
+ return
1097
+ }
1098
+
1099
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
1100
+ if let data = serviceData {
1101
+ do {
1102
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
1103
+
1104
+ // ✅ Handle duplicate transaction case
1105
+ if let status = responseObject["status"] as? Bool, status == false,
1106
+ let message = responseObject["message"] as? String,
1107
+ message.lowercased().contains("duplicate transaction") {
1108
+ self.presentPaymentErrorVC(errorMessage: message)
1109
+ return
1110
+ }
1111
+
1112
+ if let status = responseObject["status"] as? Int, status == 0,
1113
+ let message = responseObject["message"] as? String,
1114
+ message.lowercased().contains("duplicate transaction") {
1115
+ self.presentPaymentErrorVC(errorMessage: message)
1116
+ return
1117
+ }
1118
+
1119
+ // ✅ Handle generic "status == 0" error case
1120
+ if let status = responseObject["status"] as? Int, status == 0 {
1121
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
1122
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
1123
+ return
1124
+ }
1125
+ else {
1126
+ DispatchQueue.main.async {
1127
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
1128
+ paymentDoneVC.chargeData = responseObject
1129
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
1130
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
1131
+ // Pass billing and additional info
1132
+ // Conditionally pass raw FieldItem array
1133
+ paymentDoneVC.visibility = self.visibility
1134
+ paymentDoneVC.request = self.request
1135
+
1136
+ // if self.visibility?.billing == true {
1137
+ paymentDoneVC.billingInfoData = self.billingInfo
1138
+ var billingDict: [String: Any] = [:]
1139
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
1140
+ paymentDoneVC.billingInfo = billingDict
1141
+ // }
1142
+
1143
+ // if self.visibility?.additional == true {
1144
+ // Update additionalInfo values before sending
1145
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "description" }) {
1146
+ self.additionalInfo?[index].value = self.txtFieldDescription.text ?? ""
1147
+ }
1148
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "phone_number" }) {
1149
+ self.additionalInfo?[index].value = localPhone
1150
+ }
1151
+
1152
+ paymentDoneVC.additionalInfoData = self.additionalInfo
1153
+
1154
+ var additionalDict: [String: Any] = [:]
1155
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
1156
+ paymentDoneVC.additionalInfo = additionalDict
1157
+ // }
1158
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
1159
+ }
1160
+ }
1161
+ }
1162
+ } else {
1163
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
1164
+ }
1165
+ } catch let jsonError {
1166
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
1167
+ }
1168
+ } else {
1169
+ self.presentPaymentErrorVC(errorMessage: "No data received")
1170
+ }
1171
+ } else {
1172
+ if let data = serviceData,
1173
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
1174
+ let message = responseObj["message"] as? String {
1175
+ self.presentPaymentErrorVC(errorMessage: message)
1176
+ } else {
1177
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
1178
+ }
1179
+ }
1180
+ }
1181
+ task.resume()
1182
+ }
1183
+
1184
+ //MARK: - Banking Account Charge Api
1185
+ func accountChargeApi() {
1186
+
1187
+ Thread.callStackSymbols.forEach { print($0) }
1188
+
1189
+ showLoadingIndicator()
1190
+
1191
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.achCharge.path()
1192
+
1193
+ guard let serviceURL = URL(string: fullURL) else {
1194
+ hideLoadingIndicator()
1195
+ return
1196
+ }
1197
+
1198
+ var uRLRequest = URLRequest(url: serviceURL)
1199
+ uRLRequest.httpMethod = "POST"
1200
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
1201
+
1202
+ let token = UserStoreSingleton.shared.clientToken
1203
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
1204
+
1205
+ // Extract only the digits from the phone number (local only, no country code)
1206
+ let localPhone = txtFieldPhoneNumber.text?.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() ?? ""
1207
+
1208
+ var params: [String: Any] = [
1209
+ // "name": accountName ?? "",
1210
+ "name": !(request.name?.isEmpty ?? true) ? request.name! : (accountName ?? ""),
1211
+ "email": userEmail ?? "",
1212
+ "description": txtFieldDescription.text ?? "",
1213
+ "currency": "usd",
1214
+ "account_type": accountType?.lowercased() ?? "",
1215
+ "routing_number": routingNumber ?? "",
1216
+ "account_number": accountNumber ?? "",
1217
+ "payment_mode": "auth_and_capture",
1218
+ "levelIndicator": 1,
1219
+ ]
1220
+
1221
+ // ✅ Only for logged-in users
1222
+ if UserStoreSingleton.shared.isLoggedIn == true {
1223
+ let emailText = userEmail
1224
+ let emailPrefix = emailText?.components(separatedBy: "@").first ?? ""
1225
+
1226
+ params["save_account"] = (isSavedNewAccount ?? false) ? 1 : 0
1227
+
1228
+ if let customerId = UserStoreSingleton.shared.customerId, !customerId.isEmpty {
1229
+ params["customer"] = customerId
1230
+ } else {
1231
+ params["username"] = emailPrefix
1232
+ }
1233
+
1234
+ if UserStoreSingleton.shared.customerId == nil {
1235
+ params["create_customer"] = "1"
1236
+ }
1237
+ }
1238
+
1239
+ // Conditionally add billing info
1240
+ if let visibility = visibility, visibility.billing == true,
1241
+ let billing = billingInfo, !billing.isEmpty {
1242
+
1243
+ var billingInfoDict: [String: Any] = [:]
1244
+ for item in billing {
1245
+ billingInfoDict[item.name] = item.value
1246
+ }
1247
+
1248
+ params["address"] = billingInfoDict["address"] as? String ?? ""
1249
+ params["country"] = billingInfoDict["country"] as? String ?? ""
1250
+ params["state"] = billingInfoDict["state"] as? String ?? ""
1251
+ params["city"] = billingInfoDict["city"] as? String ?? ""
1252
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
1253
+ }
1254
+
1255
+ // Conditionally add additional info
1256
+ if let visibility = visibility, visibility.additional == true,
1257
+ let additional = additionalInfo, !additional.isEmpty {
1258
+ params["description"] = txtFieldDescription.text ?? ""
1259
+ params["phone_number"] = localPhone
1260
+ }
1261
+
1262
+ // Add these if recurring is enabled
1263
+ // if let req = request, req.is_recurring == true {
1264
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1265
+ // // Only send start_date if type is .custom and field is not empty
1266
+ // if let startDateText = startDate, !startDateText.isEmpty {
1267
+ // let inputFormatter = DateFormatter()
1268
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1269
+ //
1270
+ // let outputFormatter = DateFormatter()
1271
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
1272
+ //
1273
+ // if let date = inputFormatter.date(from: startDateText) {
1274
+ // let apiFormattedDate = outputFormatter.string(from: date)
1275
+ // params["start_date"] = apiFormattedDate
1276
+ // } else {
1277
+ // }
1278
+ // }
1279
+ // }
1280
+ //
1281
+ // params["interval"] = chosenPlan?.lowercased()
1282
+ // }
1283
+
1284
+ // Add these if recurring is enabled
1285
+ if let req = request, req.is_recurring == true {
1286
+ if let startDateText = startDate, !startDateText.isEmpty {
1287
+ let inputFormatter = DateFormatter()
1288
+ inputFormatter.dateFormat = "dd/MM/yyyy"
1289
+
1290
+ let outputFormatter = DateFormatter()
1291
+ outputFormatter.dateFormat = "MM/dd/yyyy"
1292
+
1293
+ if let date = inputFormatter.date(from: startDateText) {
1294
+ let apiFormattedDate = outputFormatter.string(from: date)
1295
+ params["start_date"] = apiFormattedDate
1296
+ } else {
1297
+ }
1298
+ }
1299
+
1300
+ // interval is still required
1301
+ params["interval"] = chosenPlan?.lowercased()
1302
+ }
1303
+
1304
+ // ✅ Include metadata only if it has at least 1 key-value pair
1305
+ if let metadata = request?.metadata, !metadata.isEmpty {
1306
+ params["metadata"] = metadata
1307
+ }
1308
+
1309
+
1310
+ do {
1311
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
1312
+ uRLRequest.httpBody = jsonData
1313
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1314
+ }
1315
+ } catch let 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
+ return
1329
+ }
1330
+
1331
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
1332
+ return
1333
+ }
1334
+
1335
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
1336
+ if let data = serviceData {
1337
+ do {
1338
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
1339
+
1340
+ // ✅ Handle duplicate transaction case
1341
+ if let status = responseObject["status"] as? Bool, status == false,
1342
+ let message = responseObject["message"] as? String,
1343
+ message.lowercased().contains("duplicate transaction") {
1344
+ self.presentPaymentErrorVC(errorMessage: message)
1345
+ return
1346
+ }
1347
+
1348
+ if let status = responseObject["status"] as? Int, status == 0,
1349
+ let message = responseObject["message"] as? String,
1350
+ message.lowercased().contains("duplicate transaction") {
1351
+ self.presentPaymentErrorVC(errorMessage: message)
1352
+ return
1353
+ }
1354
+
1355
+ // ✅ Handle generic "status == 0" error case
1356
+ if let status = responseObject["status"] as? Int, status == 0 {
1357
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
1358
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
1359
+ return
1360
+ }
1361
+ else {
1362
+ DispatchQueue.main.async {
1363
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
1364
+ paymentDoneVC.chargeData = responseObject
1365
+ // Pass the selected payment method
1366
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
1367
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate // Pass the delegate
1368
+ paymentDoneVC.bankPaymentParams = params
1369
+ // Pass billing and additional info
1370
+ // Conditionally pass raw FieldItem array
1371
+ paymentDoneVC.visibility = self.visibility
1372
+ paymentDoneVC.request = self.request
1373
+
1374
+ // if self.visibility?.billing == true {
1375
+ paymentDoneVC.billingInfoData = self.billingInfo
1376
+ var billingDict: [String: Any] = [:]
1377
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
1378
+ paymentDoneVC.billingInfo = billingDict
1379
+ // }
1380
+
1381
+ // if self.visibility?.additional == true {
1382
+ // Update additionalInfo values before sending
1383
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "description" }) {
1384
+ self.additionalInfo?[index].value = self.txtFieldDescription.text ?? ""
1385
+ }
1386
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "phone_number" }) {
1387
+ self.additionalInfo?[index].value = localPhone
1388
+ }
1389
+
1390
+ paymentDoneVC.additionalInfoData = self.additionalInfo
1391
+
1392
+ var additionalDict: [String: Any] = [:]
1393
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
1394
+ paymentDoneVC.additionalInfo = additionalDict
1395
+ // }
1396
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
1397
+ }
1398
+ }
1399
+ }
1400
+ } else {
1401
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
1402
+ }
1403
+ } catch let jsonError {
1404
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
1405
+ }
1406
+ } else {
1407
+ self.presentPaymentErrorVC(errorMessage: "No data received")
1408
+ }
1409
+ } else {
1410
+ if let data = serviceData,
1411
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
1412
+ let message = responseObj["message"] as? String {
1413
+ self.presentPaymentErrorVC(errorMessage: message)
1414
+ } else {
1415
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
1416
+ }
1417
+ }
1418
+ }
1419
+ task.resume()
1420
+ }
1421
+
1422
+ //MARK: - Banking Account Charge Api from Regular saved bank account
1423
+ func accountChargeSavedBankAccountApi() {
1424
+ showLoadingIndicator()
1425
+
1426
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.achCharge.path()
1427
+
1428
+ guard let serviceURL = URL(string: fullURL) else {
1429
+ hideLoadingIndicator()
1430
+ return
1431
+ }
1432
+
1433
+ var uRLRequest = URLRequest(url: serviceURL)
1434
+ uRLRequest.httpMethod = "POST"
1435
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
1436
+
1437
+ let token = UserStoreSingleton.shared.clientToken
1438
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
1439
+
1440
+ // Extract only the digits from the phone number (local only, no country code)
1441
+ let localPhone = txtFieldPhoneNumber.text?.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() ?? ""
1442
+
1443
+ // let emailText = UserStoreSingleton.shared.verificationEmail ?? ""
1444
+ // let emailPrefix = emailText.components(separatedBy: "@").first ?? ""
1445
+
1446
+ let finalEmail: String
1447
+ if let verificationEmail = UserStoreSingleton.shared.verificationEmail, !verificationEmail.isEmpty {
1448
+ finalEmail = verificationEmail
1449
+ } else {
1450
+ finalEmail = request.email ?? ""
1451
+ }
1452
+
1453
+ let emailPrefix: String
1454
+ if !finalEmail.isEmpty {
1455
+ emailPrefix = finalEmail.components(separatedBy: "@").first ?? ""
1456
+ } else {
1457
+ emailPrefix = ""
1458
+ }
1459
+
1460
+ // Determine name: use request.name if available, otherwise fallback to email prefix
1461
+ let nameParam: String
1462
+ if let requestName = request.name, !requestName.trimmingCharacters(in: .whitespaces).isEmpty {
1463
+ nameParam = requestName
1464
+ } else {
1465
+ nameParam = emailPrefix
1466
+ }
1467
+
1468
+ var params: [String: Any] = [
1469
+ // "name": UserStoreSingleton.shared.merchantName ?? "",
1470
+ "name": nameParam,
1471
+ "account_id": accountID ?? "",
1472
+ "payment_method": "ach",
1473
+ "customer": customerID ?? "",
1474
+ "currency": "usd",
1475
+ "email": finalEmail
1476
+ ]
1477
+
1478
+ // Conditionally add billing info
1479
+ if let visibility = visibility, visibility.billing == true,
1480
+ let billing = billingInfo, !billing.isEmpty {
1481
+
1482
+ var billingInfoDict: [String: Any] = [:]
1483
+ for item in billing {
1484
+ billingInfoDict[item.name] = item.value
1485
+ }
1486
+
1487
+ params["address"] = billingInfoDict["address"] as? String ?? ""
1488
+ params["country"] = billingInfoDict["country"] as? String ?? ""
1489
+ params["state"] = billingInfoDict["state"] as? String ?? ""
1490
+ params["city"] = billingInfoDict["city"] as? String ?? ""
1491
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
1492
+ }
1493
+
1494
+ // Conditionally add additional info
1495
+ if let visibility = visibility, visibility.additional == true,
1496
+ let additional = additionalInfo, !additional.isEmpty {
1497
+ params["description"] = txtFieldDescription.text ?? ""
1498
+ params["phone_number"] = localPhone
1499
+ }
1500
+
1501
+ // Add these if recurring is enabled
1502
+ // if let req = request, req.is_recurring == true {
1503
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1504
+ // // Only send start_date if type is .custom and field is not empty
1505
+ // if let startDateText = startDate, !startDateText.isEmpty {
1506
+ // let inputFormatter = DateFormatter()
1507
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1508
+ //
1509
+ // let outputFormatter = DateFormatter()
1510
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
1511
+ //
1512
+ // if let date = inputFormatter.date(from: startDateText) {
1513
+ // let apiFormattedDate = outputFormatter.string(from: date)
1514
+ // params["start_date"] = apiFormattedDate
1515
+ // } else {
1516
+ // }
1517
+ // }
1518
+ // }
1519
+ //
1520
+ // params["interval"] = chosenPlan?.lowercased()
1521
+ // }
1522
+
1523
+ // Add these if recurring is enabled
1524
+ if let req = request, req.is_recurring == true {
1525
+ if let startDateText = startDate, !startDateText.isEmpty {
1526
+ let inputFormatter = DateFormatter()
1527
+ inputFormatter.dateFormat = "dd/MM/yyyy"
1528
+
1529
+ let outputFormatter = DateFormatter()
1530
+ outputFormatter.dateFormat = "MM/dd/yyyy"
1531
+
1532
+ if let date = inputFormatter.date(from: startDateText) {
1533
+ let apiFormattedDate = outputFormatter.string(from: date)
1534
+ params["start_date"] = apiFormattedDate
1535
+ } else {
1536
+ }
1537
+ }
1538
+
1539
+ // interval is still required
1540
+ params["interval"] = chosenPlan?.lowercased()
1541
+ }
1542
+
1543
+ // ✅ Include metadata only if it has at least 1 key-value pair
1544
+ if let metadata = request?.metadata, !metadata.isEmpty {
1545
+ params["metadata"] = metadata
1546
+ }
1547
+
1548
+
1549
+ do {
1550
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
1551
+ uRLRequest.httpBody = jsonData
1552
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1553
+ }
1554
+ } catch let error {
1555
+ hideLoadingIndicator()
1556
+ return
1557
+ }
1558
+
1559
+ let session = URLSession.shared
1560
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
1561
+
1562
+ DispatchQueue.main.async {
1563
+ self.hideLoadingIndicator() // Stop loader when response is received
1564
+ }
1565
+
1566
+ if let error = error {
1567
+ return
1568
+ }
1569
+
1570
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
1571
+ return
1572
+ }
1573
+
1574
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
1575
+ if let data = serviceData {
1576
+ do {
1577
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
1578
+
1579
+ // ✅ Handle duplicate transaction case
1580
+ if let status = responseObject["status"] as? Bool, status == false,
1581
+ let message = responseObject["message"] as? String,
1582
+ message.lowercased().contains("duplicate transaction") {
1583
+ self.presentPaymentErrorVC(errorMessage: message)
1584
+ return
1585
+ }
1586
+
1587
+ if let status = responseObject["status"] as? Int, status == 0,
1588
+ let message = responseObject["message"] as? String,
1589
+ message.lowercased().contains("duplicate transaction") {
1590
+ self.presentPaymentErrorVC(errorMessage: message)
1591
+ return
1592
+ }
1593
+
1594
+ // ✅ Handle generic "status == 0" error case
1595
+ if let status = responseObject["status"] as? Int, status == 0 {
1596
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
1597
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
1598
+ return
1599
+ }
1600
+ else {
1601
+ DispatchQueue.main.async {
1602
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
1603
+ paymentDoneVC.chargeData = responseObject
1604
+ // Pass the selected payment method
1605
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
1606
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate // Pass the delegate
1607
+ paymentDoneVC.bankPaymentParams = params
1608
+ // Pass billing and additional info
1609
+ // Conditionally pass raw FieldItem array
1610
+ paymentDoneVC.visibility = self.visibility
1611
+ paymentDoneVC.request = self.request
1612
+
1613
+ // if self.visibility?.billing == true {
1614
+ paymentDoneVC.billingInfoData = self.billingInfo
1615
+ var billingDict: [String: Any] = [:]
1616
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
1617
+ paymentDoneVC.billingInfo = billingDict
1618
+ // }
1619
+
1620
+ // if self.visibility?.additional == true {
1621
+ // Update additionalInfo values before sending
1622
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "description" }) {
1623
+ self.additionalInfo?[index].value = self.txtFieldDescription.text ?? ""
1624
+ }
1625
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "phone_number" }) {
1626
+ self.additionalInfo?[index].value = localPhone
1627
+ }
1628
+
1629
+ paymentDoneVC.additionalInfoData = self.additionalInfo
1630
+
1631
+ var additionalDict: [String: Any] = [:]
1632
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
1633
+ paymentDoneVC.additionalInfo = additionalDict
1634
+ // }
1635
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
1636
+ }
1637
+ }
1638
+ }
1639
+ } else {
1640
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
1641
+ }
1642
+ } catch let jsonError {
1643
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
1644
+ }
1645
+ } else {
1646
+ self.presentPaymentErrorVC(errorMessage: "No data received")
1647
+ }
1648
+ } else {
1649
+ if let data = serviceData,
1650
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
1651
+ let message = responseObj["message"] as? String {
1652
+ self.presentPaymentErrorVC(errorMessage: message)
1653
+ } else {
1654
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
1655
+ }
1656
+ }
1657
+ }
1658
+ task.resume()
1659
+ }
1660
+
1661
+ //MARK: - Account Charge Api if user saved the account.
1662
+ func accountChargeApi(customerId: String?) {
1663
+ showLoadingIndicator()
1664
+
1665
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.achCharge.path()
1666
+
1667
+ guard let serviceURL = URL(string: fullURL) else {
1668
+ hideLoadingIndicator()
1669
+ return
1670
+ }
1671
+
1672
+ var uRLRequest = URLRequest(url: serviceURL)
1673
+ uRLRequest.httpMethod = "POST"
1674
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
1675
+
1676
+ let token = UserStoreSingleton.shared.clientToken
1677
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
1678
+
1679
+ // Extract only the digits from the phone number (local only, no country code)
1680
+ let localPhone = txtFieldPhoneNumber.text?.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() ?? ""
1681
+
1682
+ // let emailPrefix = UserStoreSingleton.shared.verificationEmail?.components(separatedBy: "@").first ?? ""
1683
+
1684
+ let finalEmail: String
1685
+ if let verificationEmail = UserStoreSingleton.shared.verificationEmail, !verificationEmail.isEmpty {
1686
+ finalEmail = verificationEmail
1687
+ } else {
1688
+ finalEmail = request.email ?? ""
1689
+ }
1690
+
1691
+ let emailPrefix: String
1692
+ if !finalEmail.isEmpty {
1693
+ emailPrefix = finalEmail.components(separatedBy: "@").first ?? ""
1694
+ } else {
1695
+ emailPrefix = ""
1696
+ }
1697
+
1698
+ var params: [String: Any] = [
1699
+ // "name": accountName ?? "",
1700
+ "name": !(request.name?.isEmpty ?? true) ? request.name! : (accountName ?? ""),
1701
+ "email": userEmail ?? "",
1702
+ "currency": "usd",
1703
+ "account_type": accountType?.lowercased() ?? "",
1704
+ "routing_number": routingNumber ?? "",
1705
+ "account_number": accountNumber ?? "",
1706
+ "payment_mode": "auth_and_capture",
1707
+ "levelIndicator": 1,
1708
+ "save_account": (isSavedNewAccount ?? false) ? 1 : 0,
1709
+ "payment_method": "ach"
1710
+ ]
1711
+
1712
+ if let customerId = customerId {
1713
+ params["customer"] = customerId
1714
+ } else {
1715
+ params["username"] = emailPrefix
1716
+ }
1717
+
1718
+ // Conditionally add billing info
1719
+ if let visibility = visibility, visibility.billing == true,
1720
+ let billing = billingInfo, !billing.isEmpty {
1721
+
1722
+ var billingInfoDict: [String: Any] = [:]
1723
+ for item in billing {
1724
+ billingInfoDict[item.name] = item.value
1725
+ }
1726
+
1727
+ params["address"] = billingInfoDict["address"] as? String ?? ""
1728
+ params["country"] = billingInfoDict["country"] as? String ?? ""
1729
+ params["state"] = billingInfoDict["state"] as? String ?? ""
1730
+ params["city"] = billingInfoDict["city"] as? String ?? ""
1731
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
1732
+ }
1733
+
1734
+ // Conditionally add additional info
1735
+ if let visibility = visibility, visibility.additional == true,
1736
+ let additional = additionalInfo, !additional.isEmpty {
1737
+
1738
+ var additionalInfoDict: [String: Any] = [:]
1739
+ for item in additional {
1740
+ additionalInfoDict[item.name] = item.value
1741
+ }
1742
+
1743
+ params["description"] = txtFieldDescription.text ?? ""
1744
+ params["phone_number"] = localPhone
1745
+ }
1746
+
1747
+ // Add these if recurring is enabled
1748
+ // if let req = request, req.is_recurring == true {
1749
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1750
+ // // Only send start_date if type is .custom and field is not empty
1751
+ // if let startDateText = startDate, !startDateText.isEmpty {
1752
+ // let inputFormatter = DateFormatter()
1753
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1754
+ //
1755
+ // let outputFormatter = DateFormatter()
1756
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
1757
+ //
1758
+ // if let date = inputFormatter.date(from: startDateText) {
1759
+ // let apiFormattedDate = outputFormatter.string(from: date)
1760
+ // params["start_date"] = apiFormattedDate
1761
+ // } else {
1762
+ // }
1763
+ // }
1764
+ // }
1765
+ //
1766
+ // params["interval"] = chosenPlan?.lowercased()
1767
+ // }
1768
+
1769
+ // Add these if recurring is enabled
1770
+ if let req = request, req.is_recurring == true {
1771
+ if let startDateText = startDate, !startDateText.isEmpty {
1772
+ let inputFormatter = DateFormatter()
1773
+ inputFormatter.dateFormat = "dd/MM/yyyy"
1774
+
1775
+ let outputFormatter = DateFormatter()
1776
+ outputFormatter.dateFormat = "MM/dd/yyyy"
1777
+
1778
+ if let date = inputFormatter.date(from: startDateText) {
1779
+ let apiFormattedDate = outputFormatter.string(from: date)
1780
+ params["start_date"] = apiFormattedDate
1781
+ } else {
1782
+ }
1783
+ }
1784
+
1785
+ // interval is still required
1786
+ params["interval"] = chosenPlan?.lowercased()
1787
+ }
1788
+
1789
+ // ✅ Include metadata only if it has at least 1 key-value pair
1790
+ if let metadata = request?.metadata, !metadata.isEmpty {
1791
+ params["metadata"] = metadata
1792
+ }
1793
+
1794
+
1795
+ do {
1796
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
1797
+ uRLRequest.httpBody = jsonData
1798
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1799
+ }
1800
+ } catch let error {
1801
+ hideLoadingIndicator()
1802
+ return
1803
+ }
1804
+
1805
+ let session = URLSession.shared
1806
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
1807
+
1808
+ DispatchQueue.main.async {
1809
+ self.hideLoadingIndicator() // Stop loader when response is received
1810
+ }
1811
+
1812
+ if let error = error {
1813
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
1814
+ return
1815
+ }
1816
+
1817
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
1818
+ self.presentPaymentErrorVC(errorMessage: "Invalid response")
1819
+ return
1820
+ }
1821
+
1822
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
1823
+ if let data = serviceData {
1824
+ do {
1825
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
1826
+
1827
+ // ✅ Handle duplicate transaction case
1828
+ if let status = responseObject["status"] as? Bool, status == false,
1829
+ let message = responseObject["message"] as? String,
1830
+ message.lowercased().contains("duplicate transaction") {
1831
+ self.presentPaymentErrorVC(errorMessage: message)
1832
+ return
1833
+ }
1834
+
1835
+ if let status = responseObject["status"] as? Int, status == 0,
1836
+ let message = responseObject["message"] as? String,
1837
+ message.lowercased().contains("duplicate transaction") {
1838
+ self.presentPaymentErrorVC(errorMessage: message)
1839
+ return
1840
+ }
1841
+
1842
+ // ✅ Handle generic "status == 0" error case
1843
+ if let status = responseObject["status"] as? Int, status == 0 {
1844
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
1845
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
1846
+ return
1847
+ }
1848
+ else {
1849
+ DispatchQueue.main.async {
1850
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
1851
+ paymentDoneVC.chargeData = responseObject
1852
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
1853
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
1854
+ paymentDoneVC.bankPaymentParams = params
1855
+ // Pass billing and additional info
1856
+ // Conditionally pass raw FieldItem array
1857
+ paymentDoneVC.visibility = self.visibility
1858
+ paymentDoneVC.request = self.request
1859
+
1860
+ // if self.visibility?.billing == true {
1861
+ paymentDoneVC.billingInfoData = self.billingInfo
1862
+ var billingDict: [String: Any] = [:]
1863
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
1864
+ paymentDoneVC.billingInfo = billingDict
1865
+ // }
1866
+
1867
+ // if self.visibility?.additional == true {
1868
+ // Update additionalInfo values before sending
1869
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "description" }) {
1870
+ self.additionalInfo?[index].value = self.txtFieldDescription.text ?? ""
1871
+ }
1872
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "phone_number" }) {
1873
+ self.additionalInfo?[index].value = localPhone
1874
+ }
1875
+
1876
+ paymentDoneVC.additionalInfoData = self.additionalInfo
1877
+
1878
+ var additionalDict: [String: Any] = [:]
1879
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
1880
+ paymentDoneVC.additionalInfo = additionalDict
1881
+ // }
1882
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
1883
+ }
1884
+ }
1885
+ }
1886
+ } else {
1887
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
1888
+ }
1889
+ } catch let jsonError {
1890
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
1891
+ }
1892
+ } else {
1893
+ self.presentPaymentErrorVC(errorMessage: "No data received")
1894
+ }
1895
+ } else {
1896
+ if let data = serviceData,
1897
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
1898
+ let message = responseObj["message"] as? String {
1899
+ self.presentPaymentErrorVC(errorMessage: message)
1900
+ } else {
1901
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
1902
+ }
1903
+ }
1904
+ }
1905
+ task.resume()
1906
+ }
1907
+
1908
+ // MARK: - 3DS Functionality
1909
+
1910
+ // MARK: - Credit Card Charge Api If Billing info is not nil and Without Login.
1911
+ func threeDSecurePaymentApi() {
1912
+ showLoadingIndicator()
1913
+
1914
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.threeDSecure.path()
1915
+
1916
+ guard let serviceURL = URL(string: fullURL) else {
1917
+ hideLoadingIndicator()
1918
+ return
1919
+ }
1920
+
1921
+ var uRLRequest = URLRequest(url: serviceURL)
1922
+ uRLRequest.httpMethod = "POST"
1923
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
1924
+
1925
+ let token = UserStoreSingleton.shared.clientToken
1926
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
1927
+
1928
+ // Extract only the digits from the phone number (local only, no country code)
1929
+ let localPhone = txtFieldPhoneNumber.text?.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() ?? ""
1930
+
1931
+ var params: [String: Any] = [
1932
+ "name": nameOnCard ?? "",
1933
+ "email": userEmail ?? "",
1934
+ "card_number": cardNumber?.replacingOccurrences(of: " ", with: "") ?? "",
1935
+ "cardholder_name": nameOnCard ?? "",
1936
+ "exp_month": expiryDate?.components(separatedBy: "/").first ?? "",
1937
+ "exp_year": expiryDate?.components(separatedBy: "/").last ?? "",
1938
+ "cvc": cvv ?? "",
1939
+ "currency": "usd",
1940
+ "tokenize": request.tokenOnly ?? false
1941
+ ]
1942
+
1943
+ // ✅ Only for logged-in users
1944
+ if UserStoreSingleton.shared.isLoggedIn == true {
1945
+ let emailText = userEmail
1946
+ let emailPrefix = emailText?.components(separatedBy: "@").first ?? ""
1947
+
1948
+ params["save_card"] = isSavedNewCard ? 1 : 0
1949
+ if isSavedNewCard {
1950
+ params["is_default"] = "1"
1951
+ }
1952
+ params["tokenize"] = request.tokenOnly ?? ""
1953
+ params["username"] = emailPrefix
1954
+
1955
+ if let customerId = UserStoreSingleton.shared.customerId {
1956
+ params["customer"] = customerId
1957
+ params["customer_id"] = customerId
1958
+ } else {
1959
+ params["create_customer"] = "1"
1960
+ }
1961
+
1962
+ if UserStoreSingleton.shared.customerId == nil {
1963
+ params["create_customer"] = "1"
1964
+ }
1965
+ }
1966
+
1967
+ // Conditionally add billing info
1968
+ if let visibility = visibility, visibility.billing == true,
1969
+ let billing = billingInfo, !billing.isEmpty {
1970
+
1971
+ var billingInfoDict: [String: Any] = [:]
1972
+ for item in billing {
1973
+ billingInfoDict[item.name] = item.value
1974
+ }
1975
+
1976
+ params["address"] = billingInfoDict["address"] as? String ?? ""
1977
+ params["country"] = billingInfoDict["country"] as? String ?? ""
1978
+ params["state"] = billingInfoDict["state"] as? String ?? ""
1979
+ params["city"] = billingInfoDict["city"] as? String ?? ""
1980
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
1981
+ }
1982
+
1983
+ // Conditionally add additional info
1984
+ if let visibility = visibility, visibility.additional == true,
1985
+ let additional = additionalInfo, !additional.isEmpty {
1986
+ params["description"] = txtFieldDescription.text ?? ""
1987
+ params["phone_number"] = localPhone
1988
+ }
1989
+
1990
+ // Add these if recurring is enabled
1991
+ // if let req = request, req.is_recurring == true {
1992
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1993
+ // // Only send start_date if type is .custom and field is not empty
1994
+ // if let startDateText = startDate, !startDateText.isEmpty {
1995
+ // let inputFormatter = DateFormatter()
1996
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1997
+ //
1998
+ // let outputFormatter = DateFormatter()
1999
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
2000
+ //
2001
+ // if let date = inputFormatter.date(from: startDateText) {
2002
+ // let apiFormattedDate = outputFormatter.string(from: date)
2003
+ // params["start_date"] = apiFormattedDate
2004
+ // } else {
2005
+ // }
2006
+ // }
2007
+ // }
2008
+ //
2009
+ // params["interval"] = chosenPlan?.lowercased()
2010
+ // }
2011
+
2012
+ // Add these if recurring is enabled
2013
+ if let req = request, req.is_recurring == true {
2014
+ if let startDateText = startDate, !startDateText.isEmpty {
2015
+ let inputFormatter = DateFormatter()
2016
+ inputFormatter.dateFormat = "dd/MM/yyyy"
2017
+
2018
+ let outputFormatter = DateFormatter()
2019
+ outputFormatter.dateFormat = "MM/dd/yyyy"
2020
+
2021
+ if let date = inputFormatter.date(from: startDateText) {
2022
+ let apiFormattedDate = outputFormatter.string(from: date)
2023
+ params["start_date"] = apiFormattedDate
2024
+ } else {
2025
+ }
2026
+ }
2027
+
2028
+ // interval is still required
2029
+ params["interval"] = chosenPlan?.lowercased()
2030
+ }
2031
+
2032
+ // ✅ Include metadata only if it has at least 1 key-value pair
2033
+ if let metadata = request?.metadata, !metadata.isEmpty {
2034
+ params["metadata"] = metadata
2035
+ }
2036
+
2037
+
2038
+ do {
2039
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
2040
+ uRLRequest.httpBody = jsonData
2041
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
2042
+ }
2043
+ } catch let error {
2044
+ hideLoadingIndicator()
2045
+ return
2046
+ }
2047
+
2048
+ let session = URLSession.shared
2049
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
2050
+
2051
+ DispatchQueue.main.async {
2052
+ self.hideLoadingIndicator() // Stop loader when response is received
2053
+ }
2054
+
2055
+ if let error = error {
2056
+ return
2057
+ }
2058
+
2059
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
2060
+ return
2061
+ }
2062
+
2063
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
2064
+ if let data = serviceData {
2065
+ do {
2066
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
2067
+
2068
+ // ✅ Handle duplicate transaction case
2069
+ if let status = responseObject["status"] as? Bool, status == false,
2070
+ let message = responseObject["message"] as? String,
2071
+ message.lowercased().contains("duplicate transaction") {
2072
+ self.presentPaymentErrorVC(errorMessage: message)
2073
+ return
2074
+ }
2075
+
2076
+ if let status = responseObject["status"] as? Int, status == 0,
2077
+ let message = responseObject["message"] as? String,
2078
+ message.lowercased().contains("duplicate transaction") {
2079
+ self.presentPaymentErrorVC(errorMessage: message)
2080
+ return
2081
+ }
2082
+
2083
+ // ✅ Handle generic "status == 0" error case
2084
+ if let status = responseObject["status"] as? Int, status == 0 {
2085
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
2086
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
2087
+ return
2088
+ }
2089
+ else {
2090
+ DispatchQueue.main.async {
2091
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "ThreeDSecurePaymentDoneVC") as? ThreeDSecurePaymentDoneVC {
2092
+
2093
+ let urlString = responseObject["redirect_url"] as? String ?? responseObject["location_url"] as? String ?? ""
2094
+ paymentDoneVC.redirectURL = urlString
2095
+ paymentDoneVC.chargeData = responseObject
2096
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
2097
+ // Pass billing and additional info
2098
+ // Conditionally pass raw FieldItem array
2099
+ paymentDoneVC.visibility = self.visibility
2100
+ paymentDoneVC.amount = self.amount
2101
+ paymentDoneVC.cardApiParams = params
2102
+ paymentDoneVC.request = self.request
2103
+
2104
+ // if self.visibility?.billing == true {
2105
+ paymentDoneVC.billingInfoData = self.billingInfo
2106
+ var billingDict: [String: Any] = [:]
2107
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
2108
+ paymentDoneVC.billingInfo = billingDict
2109
+ // }
2110
+
2111
+ // if self.visibility?.additional == true {
2112
+ // Update additionalInfo values before sending
2113
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "description" }) {
2114
+ self.additionalInfo?[index].value = self.txtFieldDescription.text ?? ""
2115
+ }
2116
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "phone_number" }) {
2117
+ self.additionalInfo?[index].value = localPhone
2118
+ }
2119
+
2120
+ paymentDoneVC.additionalInfoData = self.additionalInfo
2121
+
2122
+ var additionalDict: [String: Any] = [:]
2123
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
2124
+ paymentDoneVC.additionalInfo = additionalDict
2125
+ // }
2126
+
2127
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
2128
+ }
2129
+ }
2130
+ }
2131
+ } else {
2132
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
2133
+ }
2134
+ } catch let jsonError {
2135
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
2136
+ }
2137
+ } else {
2138
+ self.presentPaymentErrorVC(errorMessage: "No data received")
2139
+ }
2140
+ } else {
2141
+ if let data = serviceData,
2142
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
2143
+ let message = responseObj["message"] as? String {
2144
+ self.presentPaymentErrorVC(errorMessage: message)
2145
+ } else {
2146
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
2147
+ }
2148
+ }
2149
+ }
2150
+ task.resume()
2151
+ }
2152
+
2153
+ // MARK: - Credit Card Charge Api If Billing info is not nil With Login from Add New Card.
2154
+ func threeDSecurePaymentAddNewCardApi(customerId: String?) {
2155
+ showLoadingIndicator()
2156
+
2157
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.threeDSecure.path()
2158
+
2159
+ guard let serviceURL = URL(string: fullURL) else {
2160
+ hideLoadingIndicator()
2161
+ return
2162
+ }
2163
+
2164
+ var uRLRequest = URLRequest(url: serviceURL)
2165
+ uRLRequest.httpMethod = "POST"
2166
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
2167
+
2168
+ let token = UserStoreSingleton.shared.clientToken
2169
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
2170
+
2171
+ // Extract only the digits from the phone number (local only, no country code)
2172
+ let localPhone = txtFieldPhoneNumber.text?.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() ?? ""
2173
+
2174
+ var params: [String: Any] = [
2175
+ "name": nameOnCard ?? "",
2176
+ "email": userEmail ?? "",
2177
+ "card_number": cardNumber?.replacingOccurrences(of: " ", with: "") ?? "",
2178
+ "cardholder_name": nameOnCard ?? "",
2179
+ "exp_month": expiryDate?.components(separatedBy: "/").first ?? "",
2180
+ "exp_year": expiryDate?.components(separatedBy: "/").last ?? "",
2181
+ "cvc": cvv ?? "",
2182
+ "description": "Test",
2183
+ "currency": "usd",
2184
+ "tokenize": request.tokenOnly ?? false,
2185
+ "save_card": isSavedNewCard ? 1 : 0,
2186
+ "customer_id": customerId ?? ""
2187
+ ]
2188
+
2189
+ // Add is_default parameter if save_card is 1
2190
+ if isSavedNewCard {
2191
+ params["is_default"] = "1"
2192
+ }
2193
+
2194
+ // Conditionally add billing info
2195
+ if let visibility = visibility, visibility.billing == true,
2196
+ let billing = billingInfo, !billing.isEmpty {
2197
+
2198
+ var billingInfoDict: [String: Any] = [:]
2199
+ for item in billing {
2200
+ billingInfoDict[item.name] = item.value
2201
+ }
2202
+
2203
+ params["address"] = billingInfoDict["address"] as? String ?? ""
2204
+ params["country"] = billingInfoDict["country"] as? String ?? ""
2205
+ params["state"] = billingInfoDict["state"] as? String ?? ""
2206
+ params["city"] = billingInfoDict["city"] as? String ?? ""
2207
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
2208
+ }
2209
+
2210
+ // Conditionally add additional info
2211
+ if let visibility = visibility, visibility.additional == true,
2212
+ let additional = additionalInfo, !additional.isEmpty {
2213
+ params["description"] = txtFieldDescription.text ?? ""
2214
+ params["phone_number"] = localPhone
2215
+ }
2216
+
2217
+ // Add these if recurring is enabled
2218
+ // if let req = request, req.is_recurring == true {
2219
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
2220
+ // // Only send start_date if type is .custom and field is not empty
2221
+ // if let startDateText = startDate, !startDateText.isEmpty {
2222
+ // let inputFormatter = DateFormatter()
2223
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
2224
+ //
2225
+ // let outputFormatter = DateFormatter()
2226
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
2227
+ //
2228
+ // if let date = inputFormatter.date(from: startDateText) {
2229
+ // let apiFormattedDate = outputFormatter.string(from: date)
2230
+ // params["start_date"] = apiFormattedDate
2231
+ // } else {
2232
+ // }
2233
+ // }
2234
+ // }
2235
+ //
2236
+ // params["interval"] = chosenPlan?.lowercased()
2237
+ // }
2238
+
2239
+ // Add these if recurring is enabled
2240
+ if let req = request, req.is_recurring == true {
2241
+ if let startDateText = startDate, !startDateText.isEmpty {
2242
+ let inputFormatter = DateFormatter()
2243
+ inputFormatter.dateFormat = "dd/MM/yyyy"
2244
+
2245
+ let outputFormatter = DateFormatter()
2246
+ outputFormatter.dateFormat = "MM/dd/yyyy"
2247
+
2248
+ if let date = inputFormatter.date(from: startDateText) {
2249
+ let apiFormattedDate = outputFormatter.string(from: date)
2250
+ params["start_date"] = apiFormattedDate
2251
+ } else {
2252
+ }
2253
+ }
2254
+
2255
+ // interval is still required
2256
+ params["interval"] = chosenPlan?.lowercased()
2257
+ }
2258
+
2259
+ // ✅ Include metadata only if it has at least 1 key-value pair
2260
+ if let metadata = request?.metadata, !metadata.isEmpty {
2261
+ params["metadata"] = metadata
2262
+ }
2263
+
2264
+
2265
+ do {
2266
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
2267
+ uRLRequest.httpBody = jsonData
2268
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
2269
+ }
2270
+ } catch let error {
2271
+ hideLoadingIndicator()
2272
+ return
2273
+ }
2274
+
2275
+ let session = URLSession.shared
2276
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
2277
+
2278
+ DispatchQueue.main.async {
2279
+ self.hideLoadingIndicator() // Stop loader when response is received
2280
+ }
2281
+
2282
+ if let error = error {
2283
+ return
2284
+ }
2285
+
2286
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
2287
+ return
2288
+ }
2289
+
2290
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
2291
+ if let data = serviceData {
2292
+ do {
2293
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
2294
+
2295
+ // ✅ Handle duplicate transaction case
2296
+ if let status = responseObject["status"] as? Bool, status == false,
2297
+ let message = responseObject["message"] as? String,
2298
+ message.lowercased().contains("duplicate transaction") {
2299
+ self.presentPaymentErrorVC(errorMessage: message)
2300
+ return
2301
+ }
2302
+
2303
+ if let status = responseObject["status"] as? Int, status == 0,
2304
+ let message = responseObject["message"] as? String,
2305
+ message.lowercased().contains("duplicate transaction") {
2306
+ self.presentPaymentErrorVC(errorMessage: message)
2307
+ return
2308
+ }
2309
+
2310
+ // ✅ Handle generic "status == 0" error case
2311
+ if let status = responseObject["status"] as? Int, status == 0 {
2312
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
2313
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
2314
+ return
2315
+ }
2316
+ else {
2317
+ DispatchQueue.main.async {
2318
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "ThreeDSecurePaymentDoneVC") as? ThreeDSecurePaymentDoneVC {
2319
+
2320
+ let urlString = responseObject["redirect_url"] as? String ?? responseObject["location_url"] as? String ?? ""
2321
+ paymentDoneVC.redirectURL = urlString
2322
+ paymentDoneVC.chargeData = responseObject
2323
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
2324
+ // Pass billing and additional info
2325
+ // Conditionally pass raw FieldItem array
2326
+ paymentDoneVC.visibility = self.visibility
2327
+ paymentDoneVC.amount = self.amount
2328
+ paymentDoneVC.cardApiParams = params
2329
+ paymentDoneVC.request = self.request
2330
+
2331
+ // if self.visibility?.billing == true {
2332
+ paymentDoneVC.billingInfoData = self.billingInfo
2333
+ var billingDict: [String: Any] = [:]
2334
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
2335
+ paymentDoneVC.billingInfo = billingDict
2336
+ // }
2337
+
2338
+ // if self.visibility?.additional == true {
2339
+ // Update additionalInfo values before sending
2340
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "description" }) {
2341
+ self.additionalInfo?[index].value = self.txtFieldDescription.text ?? ""
2342
+ }
2343
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "phone_number" }) {
2344
+ self.additionalInfo?[index].value = localPhone
2345
+ }
2346
+
2347
+ paymentDoneVC.additionalInfoData = self.additionalInfo
2348
+
2349
+ var additionalDict: [String: Any] = [:]
2350
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
2351
+ paymentDoneVC.additionalInfo = additionalDict
2352
+ // }
2353
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
2354
+ }
2355
+ }
2356
+ }
2357
+ } else {
2358
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
2359
+ }
2360
+ } catch let jsonError {
2361
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
2362
+ }
2363
+ } else {
2364
+ self.presentPaymentErrorVC(errorMessage: "No data received")
2365
+ }
2366
+ } else {
2367
+ if let data = serviceData,
2368
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
2369
+ let message = responseObj["message"] as? String {
2370
+ self.presentPaymentErrorVC(errorMessage: message)
2371
+ } else {
2372
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
2373
+ }
2374
+ }
2375
+ }
2376
+ task.resume()
2377
+ }
2378
+
2379
+ //MARK: - GrailPay Account Charge Api if user not saved account but billing info available
2380
+ func grailPayAccountChargeApi() {
2381
+ showLoadingIndicator()
2382
+
2383
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.achCharge.path()
2384
+
2385
+ guard let serviceURL = URL(string: fullURL) else {
2386
+ hideLoadingIndicator()
2387
+ return
2388
+ }
2389
+
2390
+ var uRLRequest = URLRequest(url: serviceURL)
2391
+ uRLRequest.httpMethod = "POST"
2392
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
2393
+
2394
+ let token = UserStoreSingleton.shared.clientToken
2395
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
2396
+
2397
+ // Extract only the digits from the phone number (local only, no country code)
2398
+ let localPhone = txtFieldPhoneNumber.text?.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() ?? ""
2399
+
2400
+ var params: [String: Any] = [
2401
+ "account_id": self.grailPayAccountID ?? "",
2402
+ "account_type": self.selectedGrailPayAccountType ?? "",
2403
+ "name": self.selectedGrailPayAccountName ?? "",
2404
+ "description": "payment checkout",
2405
+ "email": userEmail ?? ""
2406
+ ]
2407
+
2408
+ // ✅ Only add these params for logged-in users
2409
+ if UserStoreSingleton.shared.isLoggedIn == true {
2410
+ let emailText = userEmail
2411
+ let emailPrefix = emailText?.components(separatedBy: "@").first ?? ""
2412
+
2413
+ params["save_account"] = (isSavedNewAccount ?? false) ? 1 : 0
2414
+ params["is_default"] = 1
2415
+ params["customer_id"] = UserStoreSingleton.shared.customerId ?? ""
2416
+
2417
+ if let customerId = UserStoreSingleton.shared.customerId, !customerId.isEmpty {
2418
+ params["customer"] = customerId
2419
+ } else {
2420
+ params["username"] = emailPrefix
2421
+ }
2422
+
2423
+ if UserStoreSingleton.shared.customerId == nil {
2424
+ params["create_customer"] = "1"
2425
+ }
2426
+ }
2427
+
2428
+ // Conditionally add billing info
2429
+ if let visibility = visibility, visibility.billing == true,
2430
+ let billing = billingInfo, !billing.isEmpty {
2431
+
2432
+ var billingInfoDict: [String: Any] = [:]
2433
+ for item in billing {
2434
+ billingInfoDict[item.name] = item.value
2435
+ }
2436
+
2437
+ params["address"] = billingInfoDict["address"] as? String ?? ""
2438
+ params["country"] = billingInfoDict["country"] as? String ?? ""
2439
+ params["state"] = billingInfoDict["state"] as? String ?? ""
2440
+ params["city"] = billingInfoDict["city"] as? String ?? ""
2441
+ params["zip"] = billingInfoDict["postal_code"] as? String ?? ""
2442
+ }
2443
+
2444
+ // Additional Info or default description
2445
+ var descriptionValue: String = "Hosted payment checkout" // default
2446
+ if let visibility = visibility, visibility.additional == true,
2447
+ let additional = additionalInfo, !additional.isEmpty {
2448
+
2449
+ var additionalDict: [String: Any] = [:]
2450
+ additional.forEach { additionalDict[$0.name] = $0.value }
2451
+
2452
+ if let desc = additionalDict["description"] as? String, !desc.isEmpty {
2453
+ descriptionValue = desc
2454
+ }
2455
+
2456
+ if let phone = additionalDict["phone_number"] as? String, !phone.isEmpty {
2457
+ params["phone_number"] = phone
2458
+ }
2459
+ }
2460
+ params["description"] = descriptionValue
2461
+
2462
+ // Add these if recurring is enabled
2463
+ // if let req = request, req.is_recurring == true {
2464
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
2465
+ // // Only send start_date if type is .custom and field is not empty
2466
+ // if let startDateText = startDate, !startDateText.isEmpty {
2467
+ // let inputFormatter = DateFormatter()
2468
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
2469
+ //
2470
+ // let outputFormatter = DateFormatter()
2471
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
2472
+ //
2473
+ // if let date = inputFormatter.date(from: startDateText) {
2474
+ // let apiFormattedDate = outputFormatter.string(from: date)
2475
+ // params["start_date"] = apiFormattedDate
2476
+ // } else {
2477
+ // }
2478
+ // }
2479
+ // }
2480
+ //
2481
+ // params["interval"] = chosenPlan?.lowercased()
2482
+ // }
2483
+
2484
+ // Add these if recurring is enabled
2485
+ if let req = request, req.is_recurring == true {
2486
+ if let startDateText = startDate, !startDateText.isEmpty {
2487
+ let inputFormatter = DateFormatter()
2488
+ inputFormatter.dateFormat = "dd/MM/yyyy"
2489
+
2490
+ let outputFormatter = DateFormatter()
2491
+ outputFormatter.dateFormat = "MM/dd/yyyy"
2492
+
2493
+ if let date = inputFormatter.date(from: startDateText) {
2494
+ let apiFormattedDate = outputFormatter.string(from: date)
2495
+ params["start_date"] = apiFormattedDate
2496
+ } else {
2497
+ }
2498
+ }
2499
+
2500
+ // interval is still required
2501
+ params["interval"] = chosenPlan?.lowercased()
2502
+ }
2503
+
2504
+ // ✅ Include metadata only if it has at least 1 key-value pair
2505
+ if let metadata = request?.metadata, !metadata.isEmpty {
2506
+ params["metadata"] = metadata
2507
+ }
2508
+
2509
+
2510
+ do {
2511
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
2512
+ uRLRequest.httpBody = jsonData
2513
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
2514
+ }
2515
+ } catch let error {
2516
+ hideLoadingIndicator()
2517
+ return
2518
+ }
2519
+
2520
+ let session = URLSession.shared
2521
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
2522
+
2523
+ DispatchQueue.main.async {
2524
+ self.hideLoadingIndicator() // Stop loader when response is received
2525
+ }
2526
+
2527
+ if let error = error {
2528
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
2529
+ return
2530
+ }
2531
+
2532
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
2533
+ self.presentPaymentErrorVC(errorMessage: "Invalid response")
2534
+ return
2535
+ }
2536
+
2537
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
2538
+ if let data = serviceData {
2539
+ do {
2540
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
2541
+
2542
+ // ✅ Handle duplicate transaction case
2543
+ if let status = responseObject["status"] as? Bool, status == false,
2544
+ let message = responseObject["message"] as? String,
2545
+ message.lowercased().contains("duplicate transaction") {
2546
+ self.presentPaymentErrorVC(errorMessage: message)
2547
+ return
2548
+ }
2549
+
2550
+ if let status = responseObject["status"] as? Int, status == 0,
2551
+ let message = responseObject["message"] as? String,
2552
+ message.lowercased().contains("duplicate transaction") {
2553
+ self.presentPaymentErrorVC(errorMessage: message)
2554
+ return
2555
+ }
2556
+
2557
+ // ✅ Handle generic "status == 0" error case
2558
+ if let status = responseObject["status"] as? Int, status == 0 {
2559
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
2560
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
2561
+ return
2562
+ }
2563
+ else {
2564
+ DispatchQueue.main.async {
2565
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
2566
+ paymentDoneVC.chargeData = responseObject
2567
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
2568
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
2569
+ paymentDoneVC.bankPaymentParams = params
2570
+ // Pass billing and additional info
2571
+ // Conditionally pass raw FieldItem array
2572
+ paymentDoneVC.visibility = self.visibility
2573
+ paymentDoneVC.request = self.request
2574
+
2575
+ // if self.visibility?.billing == true {
2576
+ paymentDoneVC.billingInfoData = self.billingInfo
2577
+ var billingDict: [String: Any] = [:]
2578
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
2579
+ paymentDoneVC.billingInfo = billingDict
2580
+ // }
2581
+
2582
+ // if self.visibility?.additional == true {
2583
+ // Update additionalInfo values before sending
2584
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "description" }) {
2585
+ self.additionalInfo?[index].value = self.txtFieldDescription.text ?? ""
2586
+ }
2587
+ if let index = self.additionalInfo?.firstIndex(where: { $0.name == "phone_number" }) {
2588
+ self.additionalInfo?[index].value = localPhone
2589
+ }
2590
+
2591
+ paymentDoneVC.additionalInfoData = self.additionalInfo
2592
+
2593
+ var additionalDict: [String: Any] = [:]
2594
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
2595
+ paymentDoneVC.additionalInfo = additionalDict
2596
+ // }
2597
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
2598
+ }
2599
+ }
2600
+ }
2601
+ } else {
2602
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
2603
+ }
2604
+ } catch let jsonError {
2605
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
2606
+ }
2607
+ } else {
2608
+ self.presentPaymentErrorVC(errorMessage: "No data received")
2609
+ }
2610
+ } else {
2611
+ if let data = serviceData,
2612
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
2613
+ let message = responseObj["message"] as? String {
2614
+ self.presentPaymentErrorVC(errorMessage: message)
2615
+ } else {
2616
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
2617
+ }
2618
+ }
2619
+ }
2620
+ task.resume()
2621
+ }
2622
+
2623
+ //MARK: - GrailPay Account Charge Api if user saved account
2624
+ func grailPayAccountChargeApi(customerId: String?) {
2625
+ showLoadingIndicator()
2626
+
2627
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.achCharge.path()
2628
+
2629
+ guard let serviceURL = URL(string: fullURL) else {
2630
+ hideLoadingIndicator()
2631
+ return
2632
+ }
2633
+
2634
+ var uRLRequest = URLRequest(url: serviceURL)
2635
+ uRLRequest.httpMethod = "POST"
2636
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
2637
+
2638
+ let token = UserStoreSingleton.shared.clientToken
2639
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
2640
+
2641
+ let emailPrefix = userEmail?.components(separatedBy: "@").first ?? ""
2642
+
2643
+ var params: [String: Any] = [
2644
+ "account_id": self.grailPayAccountID ?? "",
2645
+ "account_type": self.selectedGrailPayAccountType ?? "",
2646
+ "name": self.selectedGrailPayAccountName ?? "",
2647
+ "save_account": (isSavedForFuture ?? false) ? 1 : 0,
2648
+ "is_default": (isSavedForFuture ?? false) ? 1 : 0,
2649
+ "customer_id": customerId ?? "",
2650
+ "email": userEmail ?? "",
2651
+ "create_customer": "1",
2652
+ ]
2653
+
2654
+ if let customerId = customerId {
2655
+ params["customer"] = customerId
2656
+ } else {
2657
+ params["username"] = emailPrefix
2658
+ }
2659
+
2660
+ // // Billing Info
2661
+ // if let visibility = visibility, visibility.billing == true,
2662
+ // let billing = billingInfo, !billing.isEmpty {
2663
+ // var billingDict: [String: Any] = [:]
2664
+ // billing.forEach { billingDict[$0.name] = $0.value }
2665
+ //
2666
+ // params["address"] = billingDict["address"] as? String ?? ""
2667
+ // params["country"] = billingDict["country"] as? String ?? ""
2668
+ // params["state"] = billingDict["state"] as? String ?? ""
2669
+ // params["city"] = billingDict["city"] as? String ?? ""
2670
+ // params["zip"] = billingDict["postal_code"] as? String ?? ""
2671
+ // }
2672
+
2673
+ // Always include Billing Info if available
2674
+ if let billing = billingInfo, !billing.isEmpty {
2675
+ var billingDict: [String: Any] = [:]
2676
+ billing.forEach { billingDict[$0.name] = $0.value }
2677
+
2678
+ params["address"] = billingDict["address"] as? String ?? ""
2679
+ params["country"] = billingDict["country"] as? String ?? ""
2680
+ params["state"] = billingDict["state"] as? String ?? ""
2681
+ params["city"] = billingDict["city"] as? String ?? ""
2682
+ params["zip"] = billingDict["postal_code"] as? String ?? ""
2683
+ }
2684
+
2685
+ // // Additional Info or default description
2686
+ // var descriptionValue: String = "Hosted payment checkout" // default
2687
+ // if let visibility = visibility, visibility.additional == true,
2688
+ // let additional = additionalInfo, !additional.isEmpty {
2689
+ //
2690
+ // var additionalDict: [String: Any] = [:]
2691
+ // additional.forEach { additionalDict[$0.name] = $0.value }
2692
+ //
2693
+ // if let desc = additionalDict["description"] as? String, !desc.isEmpty {
2694
+ // descriptionValue = desc
2695
+ // }
2696
+ //
2697
+ // if let phone = additionalDict["phone_number"] as? String, !phone.isEmpty {
2698
+ // params["phone_number"] = phone
2699
+ // }
2700
+ // }
2701
+ // params["description"] = descriptionValue
2702
+
2703
+ // Always include Additional Info if available
2704
+ var descriptionValue: String = "Hosted payment checkout"
2705
+ if let additional = additionalInfo, !additional.isEmpty {
2706
+ var additionalDict: [String: Any] = [:]
2707
+ additional.forEach { additionalDict[$0.name] = $0.value }
2708
+
2709
+ if let desc = additionalDict["description"] as? String, !desc.isEmpty {
2710
+ descriptionValue = desc
2711
+ }
2712
+
2713
+ if let phone = additionalDict["phone_number"] as? String, !phone.isEmpty {
2714
+ params["phone_number"] = phone
2715
+ }
2716
+ }
2717
+ params["description"] = descriptionValue
2718
+
2719
+ // Add these if recurring is enabled
2720
+ // if let req = request, req.is_recurring == true {
2721
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
2722
+ // // Only send start_date if type is .custom and field is not empty
2723
+ // if let startDateText = startDate, !startDateText.isEmpty {
2724
+ // let inputFormatter = DateFormatter()
2725
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
2726
+ //
2727
+ // let outputFormatter = DateFormatter()
2728
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
2729
+ //
2730
+ // if let date = inputFormatter.date(from: startDateText) {
2731
+ // let apiFormattedDate = outputFormatter.string(from: date)
2732
+ // params["start_date"] = apiFormattedDate
2733
+ // } else {
2734
+ // }
2735
+ // }
2736
+ // }
2737
+ //
2738
+ // params["interval"] = chosenPlan?.lowercased()
2739
+ // }
2740
+
2741
+ // Add these if recurring is enabled
2742
+ if let req = request, req.is_recurring == true {
2743
+ if let startDateText = startDate, !startDateText.isEmpty {
2744
+ let inputFormatter = DateFormatter()
2745
+ inputFormatter.dateFormat = "dd/MM/yyyy"
2746
+
2747
+ let outputFormatter = DateFormatter()
2748
+ outputFormatter.dateFormat = "MM/dd/yyyy"
2749
+
2750
+ if let date = inputFormatter.date(from: startDateText) {
2751
+ let apiFormattedDate = outputFormatter.string(from: date)
2752
+ params["start_date"] = apiFormattedDate
2753
+ } else {
2754
+ }
2755
+ }
2756
+
2757
+ // interval is still required
2758
+ params["interval"] = chosenPlan?.lowercased()
2759
+ }
2760
+
2761
+ // ✅ Include metadata only if it has at least 1 key-value pair
2762
+ if let metadata = request?.metadata, !metadata.isEmpty {
2763
+ params["metadata"] = metadata
2764
+ }
2765
+
2766
+
2767
+ do {
2768
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
2769
+ uRLRequest.httpBody = jsonData
2770
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
2771
+ }
2772
+ } catch let error {
2773
+ hideLoadingIndicator()
2774
+ return
2775
+ }
2776
+
2777
+ let session = URLSession.shared
2778
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
2779
+
2780
+ DispatchQueue.main.async {
2781
+ self.hideLoadingIndicator() // Stop loader when response is received
2782
+ }
2783
+
2784
+ if let error = error {
2785
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
2786
+ return
2787
+ }
2788
+
2789
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
2790
+ self.presentPaymentErrorVC(errorMessage: "Invalid response")
2791
+ return
2792
+ }
2793
+
2794
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
2795
+ if let data = serviceData {
2796
+ do {
2797
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
2798
+
2799
+ // ✅ Handle duplicate transaction case
2800
+ if let status = responseObject["status"] as? Bool, status == false,
2801
+ let message = responseObject["message"] as? String,
2802
+ message.lowercased().contains("duplicate transaction") {
2803
+ self.presentPaymentErrorVC(errorMessage: message)
2804
+ return
2805
+ }
2806
+
2807
+ if let status = responseObject["status"] as? Int, status == 0,
2808
+ let message = responseObject["message"] as? String,
2809
+ message.lowercased().contains("duplicate transaction") {
2810
+ self.presentPaymentErrorVC(errorMessage: message)
2811
+ return
2812
+ }
2813
+
2814
+ // ✅ Handle generic "status == 0" error case
2815
+ if let status = responseObject["status"] as? Int, status == 0 {
2816
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
2817
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
2818
+ return
2819
+ }
2820
+ else {
2821
+ DispatchQueue.main.async {
2822
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
2823
+ paymentDoneVC.chargeData = responseObject
2824
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
2825
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
2826
+ paymentDoneVC.bankPaymentParams = params
2827
+ // Pass billing and additional info
2828
+ // Conditionally pass raw FieldItem array
2829
+ paymentDoneVC.visibility = self.visibility
2830
+ paymentDoneVC.request = self.request
2831
+
2832
+ // if self.visibility?.billing == true {
2833
+ paymentDoneVC.billingInfoData = self.billingInfo
2834
+ var billingDict: [String: Any] = [:]
2835
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
2836
+ paymentDoneVC.billingInfo = billingDict
2837
+ // }
2838
+
2839
+ // if self.visibility?.additional == true {
2840
+ paymentDoneVC.additionalInfoData = self.additionalInfo
2841
+ var additionalDict: [String: Any] = [:]
2842
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
2843
+ paymentDoneVC.additionalInfo = additionalDict
2844
+ // }
2845
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
2846
+ }
2847
+ }
2848
+ }
2849
+ } else {
2850
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
2851
+ }
2852
+ } catch let jsonError {
2853
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
2854
+ }
2855
+ } else {
2856
+ self.presentPaymentErrorVC(errorMessage: "No data received")
2857
+ }
2858
+ } else {
2859
+ if let data = serviceData,
2860
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
2861
+ let message = responseObj["message"] as? String {
2862
+ self.presentPaymentErrorVC(errorMessage: message)
2863
+ } else {
2864
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
2865
+ }
2866
+ }
2867
+ }
2868
+ task.resume()
2869
+ }
2870
+
2871
+ }
2872
+
2873
+ extension AdditionalInfoVC: UITextFieldDelegate {
2874
+
2875
+ }
2876
+
2877
+ extension AdditionalInfoVC: CountryListVCDelegate {
2878
+ func didSelectCountry(_ country: Country) {
2879
+ lblCountryCode.text = "\(country.flag ?? "") +\(country.extensionCode ?? "")"
2880
+ }
2881
+ }
2882
+
2883
+ extension String {
2884
+ static func flag(for countryCode: String) -> String {
2885
+ let base : UInt32 = 127397
2886
+ var s = ""
2887
+ for v in countryCode.uppercased().unicodeScalars {
2888
+ if let scalar = UnicodeScalar(base + v.value) {
2889
+ s.unicodeScalars.append(scalar)
2890
+ }
2891
+ }
2892
+ return s
2893
+ }
2894
+ }