@jimrising/easymerchantsdk-react-native 2.2.2 → 2.2.3

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