@jimrising/easymerchantsdk-react-native 2.5.1 → 2.5.3

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