@jimrising/easymerchantsdk-react-native 2.5.1 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/ios/Pods/Storyboard/EasyPaySdk.storyboard +9089 -0
  3. package/ios/Pods/UserDefaults/UserStoreSingleton.swift +424 -0
  4. package/ios/Pods/ViewControllers/AdditionalInfoVC.swift +2894 -0
  5. package/ios/Pods/ViewControllers/BaseVC.swift +142 -0
  6. package/ios/Pods/ViewControllers/BillingInfoVC/BillingInfoVC.swift +3686 -0
  7. package/ios/Pods/ViewControllers/BillingInfoVC/Cells/CityListTVC.swift +46 -0
  8. package/ios/Pods/ViewControllers/BillingInfoVC/Cells/CountryListTVC.swift +47 -0
  9. package/ios/Pods/ViewControllers/BillingInfoVC/Cells/StateListTVC.swift +46 -0
  10. package/ios/Pods/ViewControllers/Clean Runner_2025-07-23T14-58-05.txt +13 -0
  11. package/ios/Pods/ViewControllers/CountryListVC.swift +435 -0
  12. package/ios/Pods/ViewControllers/EmailVerificationVC.swift +286 -0
  13. package/ios/Pods/ViewControllers/GrailPayVC.swift +483 -0
  14. package/ios/Pods/ViewControllers/OTPVerificationVC.swift +2193 -0
  15. package/ios/Pods/ViewControllers/PaymentDoneVC.swift +284 -0
  16. package/ios/Pods/ViewControllers/PaymentErrorVC.swift +85 -0
  17. package/ios/Pods/ViewControllers/PaymentInformation/AccountTypeTVC.swift +41 -0
  18. package/ios/Pods/ViewControllers/PaymentInformation/PaymentInfoVC.swift +12875 -0
  19. package/ios/Pods/ViewControllers/PaymentInformation/PaymentInformationCVC.swift +35 -0
  20. package/ios/Pods/ViewControllers/PaymentInformation/RecurringTVC.swift +40 -0
  21. package/ios/Pods/ViewControllers/PaymentInformation/SavedAccountsTVC/SavedAccountTVC.swift +80 -0
  22. package/ios/Pods/ViewControllers/PaymentInformation/SavedAccountsTVC/SavedAccountTVC.xib +163 -0
  23. package/ios/Pods/ViewControllers/PaymentInformation/SavedCardsTVC/SavedCardsTVC.swift +81 -0
  24. package/ios/Pods/ViewControllers/PaymentInformation/SavedCardsTVC/SavedCardsTVC.xib +188 -0
  25. package/ios/Pods/ViewControllers/PaymentStatusWebViewVC.swift +158 -0
  26. package/ios/Pods/ViewControllers/TermAndConditionsVC.swift +63 -0
  27. package/ios/Pods/ViewControllers/ThreeDSecurePaymentDoneVC.swift +1216 -0
  28. package/ios/easymerchantsdk.podspec +1 -1
  29. package/package.json +1 -1
@@ -0,0 +1,2193 @@
1
+ //
2
+ // OTPVerificationVC.swift
3
+ // EasyPay
4
+ //
5
+ // Created by Mony's Mac on 14/08/24.
6
+ //
7
+
8
+ import UIKit
9
+
10
+ class OTPVerificationVC: BaseVC {
11
+
12
+ @IBOutlet weak var viewTextOTP1: UIView!
13
+ @IBOutlet weak var txtFieldOTPText1: UITextField!
14
+ @IBOutlet weak var viewTextOTP2: UIView!
15
+ @IBOutlet weak var txtFieldOTPText2: UITextField!
16
+ @IBOutlet weak var viewTextOTP3: UIView!
17
+ @IBOutlet weak var txtFieldOTPText3: UITextField!
18
+ @IBOutlet weak var viewTextOTP4: UIView!
19
+ @IBOutlet weak var txtFieldOTPText4: UITextField!
20
+ @IBOutlet weak var viewTextOTP5: UIView!
21
+ @IBOutlet weak var txtFieldOTPText5: UITextField!
22
+ @IBOutlet weak var viewTextOTP6: UIView!
23
+ @IBOutlet weak var txtFieldOTPText6: UITextField!
24
+ @IBOutlet weak var imgEsclamationMark: UIImageView!
25
+ @IBOutlet weak var lblOtpTimer: UILabel!
26
+ @IBOutlet weak var lblUntillResendOtp: UILabel!
27
+ @IBOutlet weak var btnResendOTP: UIButton!
28
+
29
+ @IBOutlet weak var btnConfirmCode: UIButton!
30
+ @IBOutlet weak var lblUsedSavedInfo: UILabel!
31
+ @IBOutlet weak var lblEnterOTP: UILabel!
32
+
33
+ var cardNumber: String?
34
+ var expiryDate: String?
35
+ var cvv: String?
36
+ var nameOnCard: String?
37
+ var billingInfoData: Data?
38
+
39
+ var email: String?
40
+
41
+ var selectedPaymentMethod: String?
42
+
43
+ //Banking Params
44
+ var accountName: String?
45
+ var routingNumber: String?
46
+ var accountType: String?
47
+ var accountNumber: String?
48
+
49
+ var easyPayDelegate: EasyPayViewControllerDelegate?
50
+
51
+ var easyPayVC: EasyPayViewController?
52
+
53
+ var isSavedForFuture: Bool = false
54
+
55
+ private var timeRemaining = 60 // 3 minutes
56
+ private var timer: Timer?
57
+
58
+ //GrailPay Params
59
+ var grailPayAccountID: String?
60
+ var selectedGrailPayAccountType: String?
61
+ var selectedGrailPayAccountName: String?
62
+
63
+ var request: Request!
64
+
65
+ var chosenPlan: String?
66
+ var startDate: String?
67
+
68
+ var userEmail: String?
69
+
70
+ var fieldSection: FieldSection?
71
+ var additionalInfo: [FieldItem]?
72
+ var billingInfo: [FieldItem]?
73
+ var visibility: FieldsVisibility?
74
+
75
+ // var amount: Int?
76
+ var amount: Double?
77
+
78
+ var isSavedNewCard: Bool = false
79
+
80
+ var isSavedNewAccount: Bool?
81
+ var isFrom = String()
82
+
83
+ override func viewDidLoad() {
84
+ super.viewDidLoad()
85
+ // emailVerificationApi()
86
+
87
+ uiFinishingTouchElements()
88
+
89
+ // Add tap gesture recognizer to dismiss the keyboard
90
+ let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
91
+ view.addGestureRecognizer(tapGesture)
92
+
93
+ setupTextFields()
94
+
95
+ btnResendOTP.isHidden = true
96
+
97
+
98
+ // Decode request.billingInfoData
99
+ if let billingData = request?.fields {
100
+ do {
101
+ let fieldSection = try JSONDecoder().decode(FieldSection.self, from: billingData)
102
+ self.visibility = fieldSection.visibility
103
+ self.billingInfo = fieldSection.billing
104
+ self.additionalInfo = fieldSection.additional
105
+ } catch {
106
+ }
107
+ }
108
+
109
+ decodeBillingInfo()
110
+ }
111
+
112
+ override func viewWillAppear(_ animated: Bool) {
113
+ emailVerificationApi()
114
+ startTimer()
115
+ uiFinishingTouchElements()
116
+ }
117
+
118
+ // MARK: - Decode billingInfoData
119
+ private func decodeBillingInfo() {
120
+ guard let data = billingInfoData else {
121
+ return
122
+ }
123
+
124
+ do {
125
+ let decodedSection = try JSONDecoder().decode(FieldSection.self, from: data)
126
+ self.fieldSection = decodedSection
127
+ self.billingInfo = decodedSection.billing
128
+ self.additionalInfo = decodedSection.additional
129
+ self.visibility = decodedSection.visibility
130
+
131
+ } catch {
132
+ }
133
+ }
134
+
135
+ //MARK: - Ui Colors Setup.
136
+ func uiFinishingTouchElements() {
137
+ if let containerBGcolor = UserStoreSingleton.shared.container_bg_col,
138
+ let uiColor = UIColor(hex: containerBGcolor) {
139
+ self.view.backgroundColor = uiColor
140
+ }
141
+
142
+ if let primaryFontColor = UserStoreSingleton.shared.primary_font_col,
143
+ let uiColor = UIColor(hex: primaryFontColor) {
144
+ lblUsedSavedInfo.textColor = uiColor
145
+ txtFieldOTPText1.textColor = uiColor
146
+ txtFieldOTPText2.textColor = uiColor
147
+ txtFieldOTPText3.textColor = uiColor
148
+ txtFieldOTPText4.textColor = uiColor
149
+ txtFieldOTPText5.textColor = uiColor
150
+ txtFieldOTPText6.textColor = uiColor
151
+ }
152
+
153
+ if let primaryFontColor = UserStoreSingleton.shared.secondary_font_col,
154
+ let uiColor = UIColor(hex: primaryFontColor) {
155
+ lblEnterOTP.textColor = uiColor
156
+ lblUntillResendOtp.textColor = uiColor
157
+ }
158
+
159
+ if let primaryFontColor = UserStoreSingleton.shared.primary_btn_bg_col,
160
+ let uiColor = UIColor(hex: primaryFontColor) {
161
+ btnConfirmCode.backgroundColor = uiColor
162
+ btnResendOTP.tintColor = uiColor
163
+ txtFieldOTPText1.tintColor = uiColor
164
+ txtFieldOTPText2.tintColor = uiColor
165
+ txtFieldOTPText3.tintColor = uiColor
166
+ txtFieldOTPText4.tintColor = uiColor
167
+ txtFieldOTPText5.tintColor = uiColor
168
+ txtFieldOTPText6.tintColor = uiColor
169
+ }
170
+
171
+ if let secondaryBtnBackgroundColor = UserStoreSingleton.shared.primary_btn_font_col,
172
+ let secondaryUIColor = UIColor(hex: secondaryBtnBackgroundColor) {
173
+ btnConfirmCode.setTitleColor(secondaryUIColor, for: .normal)
174
+ }
175
+
176
+ if let borderRadiusString = UserStoreSingleton.shared.border_radious,
177
+ let borderRadius = Double(borderRadiusString) { // Convert String to Double
178
+ btnConfirmCode.layer.cornerRadius = CGFloat(borderRadius) // Set corner radius
179
+ viewTextOTP1.layer.cornerRadius = CGFloat(borderRadius)
180
+ viewTextOTP2.layer.cornerRadius = CGFloat(borderRadius)
181
+ viewTextOTP3.layer.cornerRadius = CGFloat(borderRadius)
182
+ viewTextOTP4.layer.cornerRadius = CGFloat(borderRadius)
183
+ viewTextOTP5.layer.cornerRadius = CGFloat(borderRadius)
184
+ viewTextOTP6.layer.cornerRadius = CGFloat(borderRadius)
185
+ } else {
186
+ btnConfirmCode.layer.cornerRadius = 8 // Default value
187
+ }
188
+ btnConfirmCode.layer.masksToBounds = true // Ensure the corners are clipped properly
189
+
190
+ if let viewOtpPrimaryBackgroundColor = UserStoreSingleton.shared.primary_btn_bg_col,
191
+ let primaryUIColor = UIColor(hex: viewOtpPrimaryBackgroundColor),
192
+ let viewOtpSecondaryColor = UserStoreSingleton.shared.secondary_font_col,
193
+ let secondaryUIColor = UIColor(hex: viewOtpSecondaryColor) {
194
+
195
+ viewTextOTP1.layer.borderWidth = 1.0
196
+ viewTextOTP1.layer.borderColor = txtFieldOTPText1.text?.isEmpty == false ? primaryUIColor.cgColor : secondaryUIColor.cgColor
197
+ viewTextOTP2.layer.borderWidth = 1.0
198
+ viewTextOTP2.layer.borderColor = txtFieldOTPText2.text?.isEmpty == false ? primaryUIColor.cgColor : secondaryUIColor.cgColor
199
+ viewTextOTP3.layer.borderWidth = 1.0
200
+ viewTextOTP3.layer.borderColor = txtFieldOTPText3.text?.isEmpty == false ? primaryUIColor.cgColor : secondaryUIColor.cgColor
201
+ viewTextOTP4.layer.borderWidth = 1.0
202
+ viewTextOTP4.layer.borderColor = txtFieldOTPText4.text?.isEmpty == false ? primaryUIColor.cgColor : secondaryUIColor.cgColor
203
+ viewTextOTP5.layer.borderWidth = 1.0
204
+ viewTextOTP5.layer.borderColor = txtFieldOTPText5.text?.isEmpty == false ? primaryUIColor.cgColor : secondaryUIColor.cgColor
205
+ viewTextOTP6.layer.borderWidth = 1.0
206
+ viewTextOTP6.layer.borderColor = txtFieldOTPText6.text?.isEmpty == false ? primaryUIColor.cgColor : secondaryUIColor.cgColor
207
+ }
208
+
209
+ if let fontSizeString = UserStoreSingleton.shared.fontSize,
210
+ let fontSizeDouble = Double(fontSizeString) { // Convert String to Double
211
+ let fontSize = CGFloat(fontSizeDouble) // Convert Double to CGFloat
212
+ lblEnterOTP.font = UIFont.systemFont(ofSize: fontSize)
213
+ lblOtpTimer.font = UIFont.systemFont(ofSize: fontSize)
214
+ lblUntillResendOtp.font = UIFont.systemFont(ofSize: fontSize)
215
+ btnResendOTP.titleLabel?.font = UIFont.systemFont(ofSize: fontSize)
216
+ btnConfirmCode.titleLabel?.font = UIFont.systemFont(ofSize: fontSize)
217
+
218
+ txtFieldOTPText1.font = UIFont.systemFont(ofSize: fontSize)
219
+ txtFieldOTPText2.font = UIFont.systemFont(ofSize: fontSize)
220
+ txtFieldOTPText3.font = UIFont.systemFont(ofSize: fontSize)
221
+ txtFieldOTPText4.font = UIFont.systemFont(ofSize: fontSize)
222
+ txtFieldOTPText5.font = UIFont.systemFont(ofSize: fontSize)
223
+ txtFieldOTPText6.font = UIFont.systemFont(ofSize: fontSize)
224
+ }
225
+ }
226
+
227
+ // Method to dismiss the keyboard
228
+ @objc func dismissKeyboard() {
229
+ view.endEditing(true)
230
+ }
231
+
232
+ private func startTimer() {
233
+ timeRemaining = 60 // 3 minutes
234
+ lblOtpTimer.text = formatTime(timeRemaining)
235
+ timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
236
+ }
237
+
238
+ private func stopTimer() {
239
+ timer?.invalidate()
240
+ timer = nil
241
+ }
242
+
243
+ @objc private func updateTimer() {
244
+ if timeRemaining > 0 {
245
+ timeRemaining -= 1
246
+ lblOtpTimer.text = formatTime(timeRemaining)
247
+ } else {
248
+ stopTimer()
249
+ btnResendOTP.isHidden = false
250
+ imgEsclamationMark.isHidden = true
251
+ lblOtpTimer.isHidden = true
252
+ lblUntillResendOtp.isHidden = true
253
+ }
254
+ }
255
+
256
+ private func formatTime(_ seconds: Int) -> String {
257
+ let minutes = seconds / 60
258
+ let seconds = seconds % 60
259
+ return String(format: "%02d:%02d", minutes, seconds)
260
+ }
261
+
262
+ private func setupTextFields() {
263
+ let textFields = [txtFieldOTPText1, txtFieldOTPText2, txtFieldOTPText3, txtFieldOTPText4, txtFieldOTPText5, txtFieldOTPText6]
264
+
265
+ for textField in textFields {
266
+ textField?.delegate = self
267
+ textField?.textContentType = .oneTimeCode
268
+ textField?.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
269
+ }
270
+ }
271
+
272
+ //MARK: - Send OTP Email Verification Api
273
+ func emailVerificationApi() {
274
+ showLoadingIndicator()
275
+
276
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.emailVerification.path()
277
+
278
+ guard let serviceURL = URL(string: fullURL) else {
279
+ hideLoadingIndicator()
280
+ return
281
+ }
282
+
283
+ var request = URLRequest(url: serviceURL)
284
+ request.httpMethod = "POST"
285
+ request.addValue("application/json", forHTTPHeaderField: "Content-Type")
286
+
287
+ let token = UserStoreSingleton.shared.clientToken
288
+ request.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
289
+
290
+ let params: [String: Any] = [
291
+ "card_search_value": email ?? "",
292
+ "card_search_key": "email"
293
+ ]
294
+
295
+ do {
296
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
297
+ request.httpBody = jsonData
298
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
299
+ }
300
+ } catch let error {
301
+ hideLoadingIndicator()
302
+ return
303
+ }
304
+
305
+ let session = URLSession.shared
306
+ let task = session.dataTask(with: request) { (serviceData, serviceResponse, error) in
307
+
308
+ DispatchQueue.main.async {
309
+ self.hideLoadingIndicator() // Stop loader when response is received
310
+ }
311
+
312
+ if let error = error {
313
+ return
314
+ }
315
+
316
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
317
+ return
318
+ }
319
+
320
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
321
+ if let data = serviceData {
322
+ do {
323
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
324
+ DispatchQueue.main.async {
325
+ // Extract and save customer_token from send_otp response
326
+ if let dataSection = responseObject["data"] as? [String: Any],
327
+ let innerData = dataSection["data"] as? [String: Any],
328
+ let customerToken = innerData["customer_token"] as? String {
329
+ UserStoreSingleton.shared.customerToken = customerToken
330
+ }
331
+ }
332
+ } else {
333
+ }
334
+ } catch let jsonError {
335
+ }
336
+ } else {
337
+ }
338
+ } else {
339
+ }
340
+ }
341
+ task.resume()
342
+ }
343
+
344
+ //MARK: - OTP Verification Api
345
+ func otpVerificationApi() {
346
+ showLoadingIndicator()
347
+
348
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.verifyOtp.path()
349
+
350
+ guard let serviceURL = URL(string: fullURL) else {
351
+ hideLoadingIndicator()
352
+ return
353
+ }
354
+
355
+ var uRLRequest = URLRequest(url: serviceURL)
356
+ uRLRequest.httpMethod = "POST"
357
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
358
+
359
+ // Use customer_token instead of client_token for verify_otp API
360
+ let token = UserStoreSingleton.shared.customerToken
361
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Customer-Token")
362
+
363
+ let params: [String: Any] = [
364
+ "card_search_value": email ?? "",
365
+ "card_search_key": "email",
366
+ "otp": getCombinedOTP()
367
+ ]
368
+
369
+ do {
370
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
371
+ uRLRequest.httpBody = jsonData
372
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
373
+ }
374
+ } catch let error {
375
+ hideLoadingIndicator()
376
+ return
377
+ }
378
+
379
+ let session = URLSession.shared
380
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
381
+
382
+ DispatchQueue.main.async {
383
+ self.hideLoadingIndicator() // Stop loader when response is received
384
+ }
385
+
386
+ if let error = error {
387
+ return
388
+ }
389
+
390
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
391
+ return
392
+ }
393
+
394
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
395
+ if let data = serviceData {
396
+ do {
397
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
398
+ if self.selectedPaymentMethod == "Card"{
399
+ if let dataSection = responseObject["data"] as? [String: Any],
400
+ let innerData = dataSection["data"] as? [String: Any],
401
+ let customerId = innerData["customer_id"] as? String {
402
+ if self.billingInfoData == nil {
403
+ if self.request.secureAuthentication == true {
404
+ self.threeDSecurePaymentApi(customerId: customerId)
405
+ }
406
+ else {
407
+ self.paymentIntentWithSavedCardApi(customerId: customerId)
408
+ }
409
+ }
410
+ else {
411
+ // self.paymentIntentApi(customerId: customerId)
412
+ if let request = self.request {
413
+ if request.secureAuthentication == true {
414
+ self.threeDSecurePaymentSavedCardApi(customerId: customerId)
415
+ } else {
416
+ self.paymentIntentApi(customerId: customerId)
417
+ }
418
+ }
419
+ }
420
+ } else {
421
+ if let request = self.request {
422
+ if request.secureAuthentication == true {
423
+ self.threeDSecurePaymentSavedCardApi(customerId: nil)
424
+ }
425
+ else {
426
+ self.paymentIntentApi(customerId: nil)
427
+ }
428
+ }
429
+ }
430
+ }
431
+ else if self.selectedPaymentMethod == "Bank" {
432
+
433
+ if let dataSection = responseObject["data"] as? [String: Any],
434
+ let innerData = dataSection["data"] as? [String: Any],
435
+ let customerId = innerData["customer_id"] as? String {
436
+ if self.billingInfoData == nil {
437
+ self.accountChargeWithSavedAccountApi(customerId: customerId)
438
+ }
439
+ else {
440
+ self.accountChargeApi(customerId: customerId)
441
+ }
442
+ } else {
443
+ if self.billingInfoData == nil {
444
+ self.accountChargeWithSavedAccountApi(customerId: nil)
445
+ }
446
+ else {
447
+ self.accountChargeApi(customerId: nil)
448
+ }
449
+ }
450
+ }
451
+ else if self.selectedPaymentMethod == "GrailPay" {
452
+ if let dataSection = responseObject["data"] as? [String: Any],
453
+ let innerData = dataSection["data"] as? [String: Any],
454
+ let customerId = innerData["customer_id"] as? String {
455
+ self.grailPayAccountChargeApi(customerId: customerId)
456
+ } else {
457
+ self.grailPayAccountChargeApi(customerId: nil)
458
+ }
459
+ }
460
+ } else {
461
+ }
462
+ } catch let jsonError {
463
+ }
464
+ } else {
465
+ }
466
+ } else {
467
+ if let data = serviceData,
468
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
469
+ let message = responseObj["message"] as? String {
470
+ DispatchQueue.main.async {
471
+ self.showToast(message: message)
472
+ }
473
+ }
474
+ }
475
+ }
476
+ task.resume()
477
+ }
478
+
479
+ //MARK: - Card Charge Api
480
+ func paymentIntentApi(customerId: String?) {
481
+ showLoadingIndicator()
482
+
483
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.charges.path()
484
+
485
+ guard let serviceURL = URL(string: fullURL) else {
486
+ hideLoadingIndicator()
487
+ return
488
+ }
489
+
490
+ var uRLRequest = URLRequest(url: serviceURL)
491
+ uRLRequest.httpMethod = "POST"
492
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
493
+
494
+ let token = UserStoreSingleton.shared.clientToken
495
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
496
+
497
+ let emailPrefix = userEmail?.components(separatedBy: "@").first ?? ""
498
+
499
+ var params: [String: Any] = [
500
+ "name": nameOnCard ?? "",
501
+ "card_number": cardNumber?.replacingOccurrences(of: " ", with: "") ?? "",
502
+ "cardholder_name": nameOnCard ?? "",
503
+ "exp_month": expiryDate?.components(separatedBy: "/").first ?? "",
504
+ "exp_year": expiryDate?.components(separatedBy: "/").last ?? "",
505
+ "cvc": cvv ?? "",
506
+ "currency": "usd",
507
+ "payment_method": selectedPaymentMethod ?? "",
508
+ "save_card": isSavedNewCard ? 1 : 0,
509
+ // "create_customer": "1"
510
+ "email": userEmail ?? "",
511
+ // "price":"6.0",
512
+ "tokenize": request.tokenOnly ?? "",
513
+ "username": emailPrefix
514
+ ]
515
+
516
+ // Add is_default parameter if save_card is 1
517
+ if isSavedNewCard {
518
+ params["is_default"] = "1"
519
+ }
520
+
521
+ if let customerId = customerId {
522
+ params["customer"] = customerId
523
+ params["customer_id"] = customerId
524
+ }
525
+ // else {
526
+ // params["username"] = emailPrefix
527
+ // params["email"] = userEmail ?? ""
528
+ // }
529
+
530
+ if customerId == nil {
531
+ params["create_customer"] = "1"
532
+ }
533
+
534
+ if let billingInfoData = request.fields {
535
+ do {
536
+ let fieldSection = try JSONDecoder().decode(FieldSection.self, from: billingInfoData)
537
+
538
+ // Billing Info
539
+ let billing = fieldSection.billing
540
+ if !billing.isEmpty {
541
+ var billingDict: [String: Any] = [:]
542
+ billing.forEach { billingDict[$0.name] = $0.value }
543
+
544
+ if let address = billingDict["address"] as? String, !address.isEmpty {
545
+ params["address"] = address
546
+ }
547
+ if let country = billingDict["country"] as? String, !country.isEmpty {
548
+ params["country"] = country
549
+ }
550
+ if let state = billingDict["state"] as? String, !state.isEmpty {
551
+ params["state"] = state
552
+ }
553
+ if let city = billingDict["city"] as? String, !city.isEmpty {
554
+ params["city"] = city
555
+ }
556
+ if let postalCode = billingDict["postal_code"] as? String, !postalCode.isEmpty {
557
+ params["zip"] = postalCode
558
+ }
559
+ }
560
+
561
+ // Additional Info
562
+ let additional = fieldSection.additional
563
+ if !additional.isEmpty {
564
+ var additionalDict: [String: Any] = [:]
565
+ additional.forEach { additionalDict[$0.name] = $0.value }
566
+
567
+ if let desc = additionalDict["description"] as? String, !desc.isEmpty {
568
+ params["description"] = desc
569
+ } else {
570
+ params["description"] = "Hosted payment checkout"
571
+ }
572
+
573
+ if let phone = additionalDict["phone_number"] as? String, !phone.isEmpty {
574
+ params["phone_number"] = phone
575
+ }
576
+ if let email = additionalDict["email"] as? String, !email.isEmpty {
577
+ params["email"] = email
578
+ }
579
+ if let name = additionalDict["name"] as? String, !name.isEmpty {
580
+ params["name"] = name
581
+ }
582
+ } else {
583
+ // If no description in additional info, set default
584
+ params["description"] = "Hosted payment checkout"
585
+ }
586
+
587
+ } catch {
588
+ params["description"] = "Hosted payment checkout"
589
+ }
590
+ } else {
591
+ // Fallback if billingInfoData is missing
592
+ params["description"] = "Hosted payment checkout"
593
+ }
594
+
595
+ // Add these if recurring is enabled
596
+ // if let req = request, req.is_recurring == true {
597
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
598
+ // // Only send start_date if type is .custom and field is not empty
599
+ // if let startDateText = startDate, !startDateText.isEmpty {
600
+ // let inputFormatter = DateFormatter()
601
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
602
+ //
603
+ // let outputFormatter = DateFormatter()
604
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
605
+ //
606
+ // if let date = inputFormatter.date(from: startDateText) {
607
+ // let apiFormattedDate = outputFormatter.string(from: date)
608
+ // params["start_date"] = apiFormattedDate
609
+ // } else {
610
+ // }
611
+ // }
612
+ // }
613
+ //
614
+ // params["interval"] = chosenPlan?.lowercased()
615
+ // }
616
+
617
+ // Add these if recurring is enabled
618
+ if let req = request, req.is_recurring == true {
619
+ if let startDateText = startDate, !startDateText.isEmpty {
620
+ let inputFormatter = DateFormatter()
621
+ inputFormatter.dateFormat = "dd/MM/yyyy"
622
+
623
+ let outputFormatter = DateFormatter()
624
+ outputFormatter.dateFormat = "MM/dd/yyyy"
625
+
626
+ if let date = inputFormatter.date(from: startDateText) {
627
+ let apiFormattedDate = outputFormatter.string(from: date)
628
+ params["start_date"] = apiFormattedDate
629
+ } else {
630
+ }
631
+ }
632
+
633
+ // interval is still required
634
+ params["interval"] = chosenPlan?.lowercased()
635
+ }
636
+
637
+ // ✅ Include metadata only if it has at least 1 key-value pair
638
+ if let metadata = request?.metadata, !metadata.isEmpty {
639
+ params["metadata"] = metadata
640
+ }
641
+
642
+
643
+ do {
644
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
645
+ uRLRequest.httpBody = jsonData
646
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
647
+ }
648
+ } catch let error {
649
+ hideLoadingIndicator()
650
+ return
651
+ }
652
+
653
+ let session = URLSession.shared
654
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
655
+
656
+ DispatchQueue.main.async {
657
+ self.hideLoadingIndicator() // Stop loader when response is received
658
+ }
659
+
660
+ if let error = error {
661
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
662
+ return
663
+ }
664
+
665
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
666
+ self.presentPaymentErrorVC(errorMessage: "Invalid response")
667
+ return
668
+ }
669
+
670
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
671
+ if let data = serviceData {
672
+ do {
673
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
674
+
675
+ // ✅ Handle duplicate transaction case
676
+ if let status = responseObject["status"] as? Bool, status == false,
677
+ let message = responseObject["message"] as? String,
678
+ message.lowercased().contains("duplicate transaction") {
679
+ self.presentPaymentErrorVC(errorMessage: message)
680
+ return
681
+ }
682
+
683
+ if let status = responseObject["status"] as? Int, status == 0,
684
+ let message = responseObject["message"] as? String,
685
+ message.lowercased().contains("duplicate transaction") {
686
+ self.presentPaymentErrorVC(errorMessage: message)
687
+ return
688
+ }
689
+
690
+ // ✅ Handle generic "status == 0" error case
691
+ if let status = responseObject["status"] as? Int, status == 0 {
692
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
693
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
694
+ return
695
+ }
696
+ else {
697
+ DispatchQueue.main.async {
698
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
699
+ paymentDoneVC.chargeData = responseObject
700
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
701
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
702
+ paymentDoneVC.request = self.request
703
+
704
+ // Pass billing info and additional info if available
705
+ if let billingInfoData = self.request.fields,
706
+ let fieldSection = try? JSONDecoder().decode(FieldSection.self, from: billingInfoData) {
707
+
708
+ // Filter billing info: only include non-empty values
709
+ let filteredBilling = fieldSection.billing.filter { !($0.value.trimmingCharacters(in: .whitespaces).isEmpty) }
710
+ paymentDoneVC.billingInfoData = filteredBilling
711
+ var billingDict: [String: Any] = [:]
712
+ filteredBilling.forEach { billingDict[$0.name] = $0.value }
713
+ paymentDoneVC.billingInfo = billingDict
714
+
715
+ // Filter additional info: only include non-empty values
716
+ let filteredAdditional = fieldSection.additional.filter { !($0.value.trimmingCharacters(in: .whitespaces).isEmpty) }
717
+ paymentDoneVC.additionalInfoData = filteredAdditional
718
+ var additionalDict: [String: Any] = [:]
719
+ filteredAdditional.forEach { additionalDict[$0.name] = $0.value }
720
+ paymentDoneVC.additionalInfo = additionalDict
721
+ }
722
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
723
+ }
724
+ }
725
+ }
726
+ } else {
727
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
728
+ }
729
+ } catch let jsonError {
730
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
731
+ }
732
+ } else {
733
+ self.presentPaymentErrorVC(errorMessage: "No data received")
734
+ }
735
+ } else {
736
+ if let data = serviceData,
737
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
738
+ let message = responseObj["message"] as? String {
739
+ self.presentPaymentErrorVC(errorMessage: message)
740
+ } else {
741
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
742
+ }
743
+ }
744
+ }
745
+ task.resume()
746
+ }
747
+
748
+ //MARK: - Card Charge Api If billing info is nil and saved card
749
+ func paymentIntentWithSavedCardApi(customerId: String?) {
750
+ showLoadingIndicator()
751
+
752
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.charges.path()
753
+
754
+ guard let serviceURL = URL(string: fullURL) else {
755
+ hideLoadingIndicator()
756
+ return
757
+ }
758
+
759
+ var urlRequest = URLRequest(url: serviceURL)
760
+ urlRequest.httpMethod = "POST"
761
+ urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
762
+
763
+ let token = UserStoreSingleton.shared.clientToken
764
+ urlRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
765
+
766
+ let emailPrefix = email?.components(separatedBy: "@").first ?? ""
767
+
768
+ var params: [String: Any] = [
769
+ "name": emailPrefix,
770
+ "card_number": cardNumber?.replacingOccurrences(of: " ", with: "") ?? "",
771
+ "cardholder_name": nameOnCard ?? "",
772
+ "exp_month": expiryDate?.components(separatedBy: "/").first ?? "",
773
+ "exp_year": expiryDate?.components(separatedBy: "/").last ?? "",
774
+ "cvc": cvv ?? "",
775
+ "description": "description",
776
+ "currency": "usd",
777
+ "payment_method": selectedPaymentMethod ?? "",
778
+ "save_card": isSavedNewCard ? 1 : 0
779
+ ]
780
+
781
+ // Add is_default parameter if save_card is 1
782
+ if isSavedNewCard {
783
+ params["is_default"] = "1"
784
+ }
785
+
786
+ if let customerId = customerId {
787
+ params["customer"] = customerId
788
+ params["customer_id"] = customerId
789
+ } else {
790
+ params["username"] = emailPrefix
791
+ params["email"] = email ?? ""
792
+ }
793
+
794
+ if customerId == nil {
795
+ params["create_customer"] = "1"
796
+ }
797
+
798
+ // Add these if recurring is enabled
799
+ // if let req = request, req.is_recurring == true {
800
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
801
+ // // Only send start_date if type is .custom and field is not empty
802
+ // if let startDateText = startDate, !startDateText.isEmpty {
803
+ // let inputFormatter = DateFormatter()
804
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
805
+ //
806
+ // let outputFormatter = DateFormatter()
807
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
808
+ //
809
+ // if let date = inputFormatter.date(from: startDateText) {
810
+ // let apiFormattedDate = outputFormatter.string(from: date)
811
+ // params["start_date"] = apiFormattedDate
812
+ // } else {
813
+ // }
814
+ // }
815
+ // }
816
+ //
817
+ // params["interval"] = chosenPlan?.lowercased()
818
+ // }
819
+
820
+ // Add these if recurring is enabled
821
+ if let req = request, req.is_recurring == true {
822
+ if let startDateText = startDate, !startDateText.isEmpty {
823
+ let inputFormatter = DateFormatter()
824
+ inputFormatter.dateFormat = "dd/MM/yyyy"
825
+
826
+ let outputFormatter = DateFormatter()
827
+ outputFormatter.dateFormat = "MM/dd/yyyy"
828
+
829
+ if let date = inputFormatter.date(from: startDateText) {
830
+ let apiFormattedDate = outputFormatter.string(from: date)
831
+ params["start_date"] = apiFormattedDate
832
+ } else {
833
+ }
834
+ }
835
+
836
+ // interval is still required
837
+ params["interval"] = chosenPlan?.lowercased()
838
+ }
839
+
840
+ // ✅ Include metadata only if it has at least 1 key-value pair
841
+ if let metadata = request?.metadata, !metadata.isEmpty {
842
+ params["metadata"] = metadata
843
+ }
844
+
845
+
846
+ do {
847
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
848
+ urlRequest.httpBody = jsonData
849
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
850
+ }
851
+ } catch let error {
852
+ hideLoadingIndicator()
853
+ return
854
+ }
855
+
856
+ let session = URLSession.shared
857
+ let task = session.dataTask(with: urlRequest) { (serviceData, serviceResponse, error) in
858
+
859
+ DispatchQueue.main.async {
860
+ self.hideLoadingIndicator() // Stop loader when response is received
861
+ }
862
+
863
+ if let error = error {
864
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
865
+ return
866
+ }
867
+
868
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
869
+ self.presentPaymentErrorVC(errorMessage: "Invalid response")
870
+ return
871
+ }
872
+
873
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
874
+ if let data = serviceData {
875
+ do {
876
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
877
+
878
+ // ✅ Handle duplicate transaction case
879
+ if let status = responseObject["status"] as? Bool, status == false,
880
+ let message = responseObject["message"] as? String,
881
+ message.lowercased().contains("duplicate transaction") {
882
+ self.presentPaymentErrorVC(errorMessage: message)
883
+ return
884
+ }
885
+
886
+ if let status = responseObject["status"] as? Int, status == 0,
887
+ let message = responseObject["message"] as? String,
888
+ message.lowercased().contains("duplicate transaction") {
889
+ self.presentPaymentErrorVC(errorMessage: message)
890
+ return
891
+ }
892
+
893
+ // ✅ Handle generic "status == 0" error case
894
+ if let status = responseObject["status"] as? Int, status == 0 {
895
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
896
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
897
+ return
898
+ }
899
+ else {
900
+ DispatchQueue.main.async {
901
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
902
+ paymentDoneVC.chargeData = responseObject
903
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
904
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
905
+ paymentDoneVC.request = self.request
906
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
907
+ }
908
+ }
909
+ }
910
+ } else {
911
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
912
+ }
913
+ } catch let jsonError {
914
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
915
+ }
916
+ } else {
917
+ self.presentPaymentErrorVC(errorMessage: "No data received")
918
+ }
919
+ } else {
920
+ if let data = serviceData,
921
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
922
+ let message = responseObj["message"] as? String {
923
+ self.presentPaymentErrorVC(errorMessage: message)
924
+ } else {
925
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
926
+ }
927
+ }
928
+ }
929
+ task.resume()
930
+ }
931
+
932
+ func presentPaymentErrorVC(errorMessage: String) {
933
+ DispatchQueue.main.async {
934
+ if let paymentErrorVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentErrorVC") as? PaymentErrorVC {
935
+ paymentErrorVC.errorMessage = errorMessage
936
+ paymentErrorVC.easyPayDelegate = self.easyPayDelegate // Pass the reference here
937
+ self.navigationController?.pushViewController(paymentErrorVC, animated: true)
938
+ }
939
+ }
940
+ }
941
+
942
+ //MARK: - Account Charge Api
943
+ func accountChargeApi(customerId: String?) {
944
+ showLoadingIndicator()
945
+
946
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.achCharge.path()
947
+
948
+ guard let serviceURL = URL(string: fullURL) else {
949
+ hideLoadingIndicator()
950
+ return
951
+ }
952
+
953
+ var uRLRequest = URLRequest(url: serviceURL)
954
+ uRLRequest.httpMethod = "POST"
955
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
956
+
957
+ let token = UserStoreSingleton.shared.clientToken
958
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
959
+
960
+ let emailPrefix = email?.components(separatedBy: "@").first ?? ""
961
+
962
+ var params: [String: Any] = [
963
+ // "name": accountName ?? "",
964
+ "name": !(request.name?.isEmpty ?? true) ? request.name! : (accountName ?? ""),
965
+ "email": userEmail ?? "",
966
+ "currency": "usd",
967
+ "account_type": accountType?.lowercased() ?? "",
968
+ "routing_number": routingNumber ?? "",
969
+ "account_number": accountNumber ?? "",
970
+ "payment_mode": "auth_and_capture",
971
+ "levelIndicator": 1,
972
+ "save_account": (isSavedNewAccount ?? false) ? 1 : 0,
973
+ "payment_method": "ach"
974
+ ]
975
+
976
+ if let customerId = customerId {
977
+ params["customer"] = customerId
978
+ } else {
979
+ params["username"] = emailPrefix
980
+ }
981
+
982
+ if customerId == nil {
983
+ params["create_customer"] = "1"
984
+ }
985
+
986
+ if let billingInfoData = request.fields {
987
+ do {
988
+ let fieldSection = try JSONDecoder().decode(FieldSection.self, from: billingInfoData)
989
+
990
+ // Billing Info
991
+ let billing = fieldSection.billing
992
+ if !billing.isEmpty {
993
+ var billingDict: [String: Any] = [:]
994
+ billing.forEach { billingDict[$0.name] = $0.value }
995
+
996
+ if let address = billingDict["address"] as? String, !address.isEmpty {
997
+ params["address"] = address
998
+ }
999
+ if let country = billingDict["country"] as? String, !country.isEmpty {
1000
+ params["country"] = country
1001
+ }
1002
+ if let state = billingDict["state"] as? String, !state.isEmpty {
1003
+ params["state"] = state
1004
+ }
1005
+ if let city = billingDict["city"] as? String, !city.isEmpty {
1006
+ params["city"] = city
1007
+ }
1008
+ if let postalCode = billingDict["postal_code"] as? String, !postalCode.isEmpty {
1009
+ params["zip"] = postalCode
1010
+ }
1011
+ }
1012
+
1013
+ // Additional Info
1014
+ let additional = fieldSection.additional
1015
+ if !additional.isEmpty {
1016
+ var additionalDict: [String: Any] = [:]
1017
+ additional.forEach { additionalDict[$0.name] = $0.value }
1018
+
1019
+ if let desc = additionalDict["description"] as? String, !desc.isEmpty {
1020
+ params["description"] = desc
1021
+ } else {
1022
+ params["description"] = "Hosted payment checkout"
1023
+ }
1024
+
1025
+ if let phone = additionalDict["phone_number"] as? String, !phone.isEmpty {
1026
+ params["phone_number"] = phone
1027
+ }
1028
+ if let email = additionalDict["email"] as? String, !email.isEmpty {
1029
+ params["email"] = email
1030
+ }
1031
+ if let name = additionalDict["name"] as? String, !name.isEmpty {
1032
+ params["name"] = name
1033
+ }
1034
+ } else {
1035
+ // If no description in additional info, set default
1036
+ params["description"] = "Hosted payment checkout"
1037
+ }
1038
+
1039
+ } catch {
1040
+ params["description"] = "Hosted payment checkout"
1041
+ }
1042
+ } else {
1043
+ // Fallback if billingInfoData is missing
1044
+ params["description"] = "Hosted payment checkout"
1045
+ }
1046
+
1047
+ // Add these if recurring is enabled
1048
+ // if let req = request, req.is_recurring == true {
1049
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1050
+ // // Only send start_date if type is .custom and field is not empty
1051
+ // if let startDateText = startDate, !startDateText.isEmpty {
1052
+ // let inputFormatter = DateFormatter()
1053
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1054
+ //
1055
+ // let outputFormatter = DateFormatter()
1056
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
1057
+ //
1058
+ // if let date = inputFormatter.date(from: startDateText) {
1059
+ // let apiFormattedDate = outputFormatter.string(from: date)
1060
+ // params["start_date"] = apiFormattedDate
1061
+ // } else {
1062
+ // }
1063
+ // }
1064
+ // }
1065
+ //
1066
+ // params["interval"] = chosenPlan?.lowercased()
1067
+ // }
1068
+
1069
+ // Add these if recurring is enabled
1070
+ if let req = request, req.is_recurring == true {
1071
+ if let startDateText = startDate, !startDateText.isEmpty {
1072
+ let inputFormatter = DateFormatter()
1073
+ inputFormatter.dateFormat = "dd/MM/yyyy"
1074
+
1075
+ let outputFormatter = DateFormatter()
1076
+ outputFormatter.dateFormat = "MM/dd/yyyy"
1077
+
1078
+ if let date = inputFormatter.date(from: startDateText) {
1079
+ let apiFormattedDate = outputFormatter.string(from: date)
1080
+ params["start_date"] = apiFormattedDate
1081
+ } else {
1082
+ }
1083
+ }
1084
+
1085
+ // interval is still required
1086
+ params["interval"] = chosenPlan?.lowercased()
1087
+ }
1088
+
1089
+ // ✅ Include metadata only if it has at least 1 key-value pair
1090
+ if let metadata = request?.metadata, !metadata.isEmpty {
1091
+ params["metadata"] = metadata
1092
+ }
1093
+
1094
+
1095
+ do {
1096
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
1097
+ uRLRequest.httpBody = jsonData
1098
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1099
+ }
1100
+ } catch let error {
1101
+ hideLoadingIndicator()
1102
+ return
1103
+ }
1104
+
1105
+ let session = URLSession.shared
1106
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
1107
+
1108
+ DispatchQueue.main.async {
1109
+ self.hideLoadingIndicator() // Stop loader when response is received
1110
+ }
1111
+
1112
+ if let error = error {
1113
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
1114
+ return
1115
+ }
1116
+
1117
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
1118
+ self.presentPaymentErrorVC(errorMessage: "Invalid response")
1119
+ return
1120
+ }
1121
+
1122
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
1123
+ if let data = serviceData {
1124
+ do {
1125
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
1126
+
1127
+ // ✅ Handle duplicate transaction case
1128
+ if let status = responseObject["status"] as? Bool, status == false,
1129
+ let message = responseObject["message"] as? String,
1130
+ message.lowercased().contains("duplicate transaction") {
1131
+ self.presentPaymentErrorVC(errorMessage: message)
1132
+ return
1133
+ }
1134
+
1135
+ if let status = responseObject["status"] as? Int, status == 0,
1136
+ let message = responseObject["message"] as? String,
1137
+ message.lowercased().contains("duplicate transaction") {
1138
+ self.presentPaymentErrorVC(errorMessage: message)
1139
+ return
1140
+ }
1141
+
1142
+ // ✅ Handle generic "status == 0" error case
1143
+ if let status = responseObject["status"] as? Int, status == 0 {
1144
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
1145
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
1146
+ return
1147
+ }
1148
+ else {
1149
+ DispatchQueue.main.async {
1150
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
1151
+ paymentDoneVC.chargeData = responseObject
1152
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
1153
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
1154
+ paymentDoneVC.bankPaymentParams = params
1155
+ paymentDoneVC.request = self.request
1156
+ // Pass billing info and additional info if available
1157
+ if let billingInfoData = self.request.fields,
1158
+ let fieldSection = try? JSONDecoder().decode(FieldSection.self, from: billingInfoData) {
1159
+
1160
+ // Filter billing info: only include non-empty values
1161
+ let filteredBilling = fieldSection.billing.filter { !($0.value.trimmingCharacters(in: .whitespaces).isEmpty) }
1162
+ paymentDoneVC.billingInfoData = filteredBilling
1163
+ var billingDict: [String: Any] = [:]
1164
+ filteredBilling.forEach { billingDict[$0.name] = $0.value }
1165
+ paymentDoneVC.billingInfo = billingDict
1166
+
1167
+ // Filter additional info: only include non-empty values
1168
+ let filteredAdditional = fieldSection.additional.filter { !($0.value.trimmingCharacters(in: .whitespaces).isEmpty) }
1169
+ paymentDoneVC.additionalInfoData = filteredAdditional
1170
+ var additionalDict: [String: Any] = [:]
1171
+ filteredAdditional.forEach { additionalDict[$0.name] = $0.value }
1172
+ paymentDoneVC.additionalInfo = additionalDict
1173
+ }
1174
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
1175
+ }
1176
+ }
1177
+ }
1178
+ } else {
1179
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
1180
+ }
1181
+ } catch let jsonError {
1182
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
1183
+ }
1184
+ } else {
1185
+ self.presentPaymentErrorVC(errorMessage: "No data received")
1186
+ }
1187
+ } else {
1188
+ if let data = serviceData,
1189
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
1190
+ let message = responseObj["message"] as? String {
1191
+ self.presentPaymentErrorVC(errorMessage: message)
1192
+ } else {
1193
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
1194
+ }
1195
+ }
1196
+ }
1197
+ task.resume()
1198
+ }
1199
+
1200
+ //MARK: - Account Charge Api if billing info is nil and saved account
1201
+ func accountChargeWithSavedAccountApi(customerId: String?) {
1202
+ showLoadingIndicator()
1203
+
1204
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.achCharge.path()
1205
+
1206
+ guard let serviceURL = URL(string: fullURL) else {
1207
+ hideLoadingIndicator()
1208
+ return
1209
+ }
1210
+
1211
+ var uRLRequest = URLRequest(url: serviceURL)
1212
+ uRLRequest.httpMethod = "POST"
1213
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
1214
+
1215
+ let token = UserStoreSingleton.shared.clientToken
1216
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
1217
+
1218
+ let emailPrefix = email?.components(separatedBy: "@").first ?? ""
1219
+
1220
+ var params: [String: Any] = [
1221
+ // "name": emailPrefix,
1222
+ "name": !(request.name?.isEmpty ?? true) ? request.name! : (emailPrefix),
1223
+ "email": email ?? "",
1224
+ "description": "Test Description",
1225
+ "currency": "usd",
1226
+ "account_type": accountType?.lowercased() ?? "",
1227
+ "routing_number": routingNumber ?? "",
1228
+ "account_number": accountNumber ?? "",
1229
+ "payment_mode": "auth_and_capture",
1230
+ "levelIndicator": 1,
1231
+ "save_account": (isSavedNewAccount ?? false) ? 1 : 0,
1232
+ "payment_method": "ach"
1233
+ ]
1234
+
1235
+ if let customerId = customerId {
1236
+ params["customer"] = customerId
1237
+ } else {
1238
+ params["username"] = emailPrefix
1239
+ }
1240
+
1241
+ if customerId == nil {
1242
+ params["create_customer"] = "1"
1243
+ }
1244
+
1245
+ // Add these if recurring is enabled
1246
+ // if let req = request, req.is_recurring == true {
1247
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1248
+ // // Only send start_date if type is .custom and field is not empty
1249
+ // if let startDateText = startDate, !startDateText.isEmpty {
1250
+ // let inputFormatter = DateFormatter()
1251
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1252
+ //
1253
+ // let outputFormatter = DateFormatter()
1254
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
1255
+ //
1256
+ // if let date = inputFormatter.date(from: startDateText) {
1257
+ // let apiFormattedDate = outputFormatter.string(from: date)
1258
+ // params["start_date"] = apiFormattedDate
1259
+ // } else {
1260
+ // }
1261
+ // }
1262
+ // }
1263
+ //
1264
+ // params["interval"] = chosenPlan?.lowercased()
1265
+ // }
1266
+
1267
+ // Add these if recurring is enabled
1268
+ if let req = request, req.is_recurring == true {
1269
+ if let startDateText = startDate, !startDateText.isEmpty {
1270
+ let inputFormatter = DateFormatter()
1271
+ inputFormatter.dateFormat = "dd/MM/yyyy"
1272
+
1273
+ let outputFormatter = DateFormatter()
1274
+ outputFormatter.dateFormat = "MM/dd/yyyy"
1275
+
1276
+ if let date = inputFormatter.date(from: startDateText) {
1277
+ let apiFormattedDate = outputFormatter.string(from: date)
1278
+ params["start_date"] = apiFormattedDate
1279
+ } else {
1280
+ }
1281
+ }
1282
+
1283
+ // interval is still required
1284
+ params["interval"] = chosenPlan?.lowercased()
1285
+ }
1286
+
1287
+ // ✅ Include metadata only if it has at least 1 key-value pair
1288
+ if let metadata = request?.metadata, !metadata.isEmpty {
1289
+ params["metadata"] = metadata
1290
+ }
1291
+
1292
+
1293
+ do {
1294
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
1295
+ uRLRequest.httpBody = jsonData
1296
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1297
+ }
1298
+ } catch let error {
1299
+ hideLoadingIndicator()
1300
+ return
1301
+ }
1302
+
1303
+ let session = URLSession.shared
1304
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
1305
+
1306
+ DispatchQueue.main.async {
1307
+ self.hideLoadingIndicator() // Stop loader when response is received
1308
+ }
1309
+
1310
+ if let error = error {
1311
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
1312
+ return
1313
+ }
1314
+
1315
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
1316
+ self.presentPaymentErrorVC(errorMessage: "Invalid response")
1317
+ return
1318
+ }
1319
+
1320
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
1321
+ if let data = serviceData {
1322
+ do {
1323
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
1324
+
1325
+ // ✅ Handle duplicate transaction case
1326
+ if let status = responseObject["status"] as? Bool, status == false,
1327
+ let message = responseObject["message"] as? String,
1328
+ message.lowercased().contains("duplicate transaction") {
1329
+ self.presentPaymentErrorVC(errorMessage: message)
1330
+ return
1331
+ }
1332
+
1333
+ if let status = responseObject["status"] as? Int, status == 0,
1334
+ let message = responseObject["message"] as? String,
1335
+ message.lowercased().contains("duplicate transaction") {
1336
+ self.presentPaymentErrorVC(errorMessage: message)
1337
+ return
1338
+ }
1339
+
1340
+ // ✅ Handle generic "status == 0" error case
1341
+ if let status = responseObject["status"] as? Int, status == 0 {
1342
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
1343
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
1344
+ return
1345
+ }
1346
+ else {
1347
+ DispatchQueue.main.async {
1348
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
1349
+ paymentDoneVC.chargeData = responseObject
1350
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
1351
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
1352
+ paymentDoneVC.bankPaymentParams = params
1353
+ paymentDoneVC.request = self.request
1354
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
1355
+ }
1356
+ }
1357
+ }
1358
+ } else {
1359
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
1360
+ }
1361
+ } catch let jsonError {
1362
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
1363
+ }
1364
+ } else {
1365
+ self.presentPaymentErrorVC(errorMessage: "No data received")
1366
+ }
1367
+ } else {
1368
+ if let data = serviceData,
1369
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
1370
+ let message = responseObj["message"] as? String {
1371
+ self.presentPaymentErrorVC(errorMessage: message)
1372
+ } else {
1373
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
1374
+ }
1375
+ }
1376
+ }
1377
+ task.resume()
1378
+ }
1379
+
1380
+ //MARK: - GrailPay Account Charge Api if user saved account
1381
+ func grailPayAccountChargeApi(customerId: String?) {
1382
+ showLoadingIndicator()
1383
+
1384
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.achCharge.path()
1385
+
1386
+ guard let serviceURL = URL(string: fullURL) else {
1387
+ hideLoadingIndicator()
1388
+ return
1389
+ }
1390
+
1391
+ var uRLRequest = URLRequest(url: serviceURL)
1392
+ uRLRequest.httpMethod = "POST"
1393
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
1394
+
1395
+ let token = UserStoreSingleton.shared.clientToken
1396
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
1397
+
1398
+ let emailPrefix = userEmail?.components(separatedBy: "@").first ?? ""
1399
+
1400
+ var params: [String: Any] = [
1401
+ "account_id": self.grailPayAccountID ?? "",
1402
+ "account_type": self.selectedGrailPayAccountType ?? "",
1403
+ "name": self.selectedGrailPayAccountName ?? "",
1404
+ "save_account": (isSavedForFuture ?? false) ? 1 : 0,
1405
+ "is_default": (isSavedForFuture ?? false) ? 1 : 0,
1406
+ "customer_id": customerId ?? "",
1407
+ "email": userEmail ?? "",
1408
+ "create_customer": "1",
1409
+ ]
1410
+
1411
+ if let customerId = customerId {
1412
+ params["customer"] = customerId
1413
+ } else {
1414
+ params["username"] = emailPrefix
1415
+ }
1416
+
1417
+ if customerId == nil {
1418
+ params["create_customer"] = "1"
1419
+ }
1420
+
1421
+ // // Billing Info
1422
+ // if let visibility = visibility, visibility.billing == true,
1423
+ // let billing = billingInfo, !billing.isEmpty {
1424
+ // var billingDict: [String: Any] = [:]
1425
+ // billing.forEach { billingDict[$0.name] = $0.value }
1426
+ //
1427
+ // params["address"] = billingDict["address"] as? String ?? ""
1428
+ // params["country"] = billingDict["country"] as? String ?? ""
1429
+ // params["state"] = billingDict["state"] as? String ?? ""
1430
+ // params["city"] = billingDict["city"] as? String ?? ""
1431
+ // params["zip"] = billingDict["postal_code"] as? String ?? ""
1432
+ // }
1433
+
1434
+ // Always include Billing Info if available
1435
+ if let billing = billingInfo, !billing.isEmpty {
1436
+ var billingDict: [String: Any] = [:]
1437
+ billing.forEach { billingDict[$0.name] = $0.value }
1438
+
1439
+ params["address"] = billingDict["address"] as? String ?? ""
1440
+ params["country"] = billingDict["country"] as? String ?? ""
1441
+ params["state"] = billingDict["state"] as? String ?? ""
1442
+ params["city"] = billingDict["city"] as? String ?? ""
1443
+ params["zip"] = billingDict["postal_code"] as? String ?? ""
1444
+ }
1445
+
1446
+ // // Additional Info or default description
1447
+ // var descriptionValue: String = "Hosted payment checkout" // default
1448
+ // if let visibility = visibility, visibility.additional == true,
1449
+ // let additional = additionalInfo, !additional.isEmpty {
1450
+ //
1451
+ // var additionalDict: [String: Any] = [:]
1452
+ // additional.forEach { additionalDict[$0.name] = $0.value }
1453
+ //
1454
+ // if let desc = additionalDict["description"] as? String, !desc.isEmpty {
1455
+ // descriptionValue = desc
1456
+ // }
1457
+ //
1458
+ // if let phone = additionalDict["phone_number"] as? String, !phone.isEmpty {
1459
+ // params["phone_number"] = phone
1460
+ // }
1461
+ // }
1462
+ // params["description"] = descriptionValue
1463
+
1464
+ // Always include Additional Info if available
1465
+ var descriptionValue: String = "Hosted payment checkout"
1466
+ if let additional = additionalInfo, !additional.isEmpty {
1467
+ var additionalDict: [String: Any] = [:]
1468
+ additional.forEach { additionalDict[$0.name] = $0.value }
1469
+
1470
+ if let desc = additionalDict["description"] as? String, !desc.isEmpty {
1471
+ descriptionValue = desc
1472
+ }
1473
+
1474
+ if let phone = additionalDict["phone_number"] as? String, !phone.isEmpty {
1475
+ params["phone_number"] = phone
1476
+ }
1477
+ }
1478
+ params["description"] = descriptionValue
1479
+
1480
+ // Add these if recurring is enabled
1481
+ // if let req = request, req.is_recurring == true {
1482
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1483
+ // // Only send start_date if type is .custom and field is not empty
1484
+ // if let startDateText = startDate, !startDateText.isEmpty {
1485
+ // let inputFormatter = DateFormatter()
1486
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1487
+ //
1488
+ // let outputFormatter = DateFormatter()
1489
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
1490
+ //
1491
+ // if let date = inputFormatter.date(from: startDateText) {
1492
+ // let apiFormattedDate = outputFormatter.string(from: date)
1493
+ // params["start_date"] = apiFormattedDate
1494
+ // } else {
1495
+ // }
1496
+ // }
1497
+ // }
1498
+ //
1499
+ // params["interval"] = chosenPlan?.lowercased()
1500
+ // }
1501
+
1502
+ // Add these if recurring is enabled
1503
+ if let req = request, req.is_recurring == true {
1504
+ if let startDateText = startDate, !startDateText.isEmpty {
1505
+ let inputFormatter = DateFormatter()
1506
+ inputFormatter.dateFormat = "dd/MM/yyyy"
1507
+
1508
+ let outputFormatter = DateFormatter()
1509
+ outputFormatter.dateFormat = "MM/dd/yyyy"
1510
+
1511
+ if let date = inputFormatter.date(from: startDateText) {
1512
+ let apiFormattedDate = outputFormatter.string(from: date)
1513
+ params["start_date"] = apiFormattedDate
1514
+ } else {
1515
+ }
1516
+ }
1517
+
1518
+ // interval is still required
1519
+ params["interval"] = chosenPlan?.lowercased()
1520
+ }
1521
+
1522
+ // ✅ Include metadata only if it has at least 1 key-value pair
1523
+ if let metadata = request?.metadata, !metadata.isEmpty {
1524
+ params["metadata"] = metadata
1525
+ }
1526
+
1527
+
1528
+ do {
1529
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
1530
+ uRLRequest.httpBody = jsonData
1531
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1532
+ }
1533
+ } catch let error {
1534
+ hideLoadingIndicator()
1535
+ return
1536
+ }
1537
+
1538
+ let session = URLSession.shared
1539
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
1540
+
1541
+ DispatchQueue.main.async {
1542
+ self.hideLoadingIndicator() // Stop loader when response is received
1543
+ }
1544
+
1545
+ if let error = error {
1546
+ self.presentPaymentErrorVC(errorMessage: error.localizedDescription)
1547
+ return
1548
+ }
1549
+
1550
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
1551
+ self.presentPaymentErrorVC(errorMessage: "Invalid response")
1552
+ return
1553
+ }
1554
+
1555
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
1556
+ if let data = serviceData {
1557
+ do {
1558
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
1559
+
1560
+ // ✅ Handle duplicate transaction case
1561
+ if let status = responseObject["status"] as? Bool, status == false,
1562
+ let message = responseObject["message"] as? String,
1563
+ message.lowercased().contains("duplicate transaction") {
1564
+ self.presentPaymentErrorVC(errorMessage: message)
1565
+ return
1566
+ }
1567
+
1568
+ if let status = responseObject["status"] as? Int, status == 0,
1569
+ let message = responseObject["message"] as? String,
1570
+ message.lowercased().contains("duplicate transaction") {
1571
+ self.presentPaymentErrorVC(errorMessage: message)
1572
+ return
1573
+ }
1574
+
1575
+ // ✅ Handle generic "status == 0" error case
1576
+ if let status = responseObject["status"] as? Int, status == 0 {
1577
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
1578
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
1579
+ return
1580
+ }
1581
+ else {
1582
+ DispatchQueue.main.async {
1583
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "PaymentDoneVC") as? PaymentDoneVC {
1584
+ paymentDoneVC.chargeData = responseObject
1585
+ paymentDoneVC.selectedPaymentMethod = self.selectedPaymentMethod
1586
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
1587
+ paymentDoneVC.bankPaymentParams = params
1588
+ // Pass billing and additional info
1589
+ // Conditionally pass raw FieldItem array
1590
+ paymentDoneVC.visibility = self.visibility
1591
+ paymentDoneVC.request = self.request
1592
+
1593
+ // if self.visibility?.billing == true {
1594
+ paymentDoneVC.billingInfoData = self.billingInfo
1595
+ var billingDict: [String: Any] = [:]
1596
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
1597
+ paymentDoneVC.billingInfo = billingDict
1598
+ // }
1599
+
1600
+ // if self.visibility?.additional == true {
1601
+ paymentDoneVC.additionalInfoData = self.additionalInfo
1602
+ var additionalDict: [String: Any] = [:]
1603
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
1604
+ paymentDoneVC.additionalInfo = additionalDict
1605
+ // }
1606
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
1607
+ }
1608
+ }
1609
+ }
1610
+ } else {
1611
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
1612
+ }
1613
+ } catch let jsonError {
1614
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
1615
+ }
1616
+ } else {
1617
+ self.presentPaymentErrorVC(errorMessage: "No data received")
1618
+ }
1619
+ } else {
1620
+ if let data = serviceData,
1621
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
1622
+ let message = responseObj["message"] as? String {
1623
+ self.presentPaymentErrorVC(errorMessage: message)
1624
+ } else {
1625
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
1626
+ }
1627
+ }
1628
+ }
1629
+ task.resume()
1630
+ }
1631
+
1632
+ // MARK: - 3DS Functionality
1633
+
1634
+ // MARK: - Credit Card Charge Api If Billing info is nil and Without Login.
1635
+ func threeDSecurePaymentApi(customerId: String?) {
1636
+ showLoadingIndicator()
1637
+
1638
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.threeDSecure.path()
1639
+
1640
+ guard let serviceURL = URL(string: fullURL) else {
1641
+ hideLoadingIndicator()
1642
+ return
1643
+ }
1644
+
1645
+ var uRLRequest = URLRequest(url: serviceURL)
1646
+ uRLRequest.httpMethod = "POST"
1647
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
1648
+
1649
+ let token = UserStoreSingleton.shared.clientToken
1650
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
1651
+
1652
+ var params: [String: Any] = [
1653
+ "name": nameOnCard ?? "",
1654
+ "email": userEmail ?? "",
1655
+ "card_number": cardNumber?.replacingOccurrences(of: " ", with: "") ?? "",
1656
+ "cardholder_name": nameOnCard ?? "",
1657
+ "exp_month": expiryDate?.components(separatedBy: "/").first ?? "",
1658
+ "exp_year": expiryDate?.components(separatedBy: "/").last ?? "",
1659
+ "cvc": cvv ?? "",
1660
+ "description": "payment checkout",
1661
+ "currency": "usd",
1662
+ "payment_method": "card",
1663
+ "tokenize": request.tokenOnly ?? false,
1664
+ "save_card": isSavedNewCard ? 1 : 0,
1665
+ "customer_id": customerId ?? ""
1666
+ ]
1667
+
1668
+ // Add is_default parameter if save_card is 1
1669
+ if isSavedNewCard {
1670
+ params["is_default"] = "1"
1671
+ }
1672
+
1673
+ if customerId == nil {
1674
+ params["create_customer"] = "1"
1675
+ }
1676
+
1677
+ // Add these if recurring is enabled
1678
+ // if let req = request, req.is_recurring == true {
1679
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1680
+ // // Only send start_date if type is .custom and field is not empty
1681
+ // if let startDateText = startDate, !startDateText.isEmpty {
1682
+ // let inputFormatter = DateFormatter()
1683
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1684
+ //
1685
+ // let outputFormatter = DateFormatter()
1686
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
1687
+ //
1688
+ // if let date = inputFormatter.date(from: startDateText) {
1689
+ // let apiFormattedDate = outputFormatter.string(from: date)
1690
+ // params["start_date"] = apiFormattedDate
1691
+ // } else {
1692
+ // }
1693
+ // }
1694
+ // }
1695
+ //
1696
+ // params["interval"] = chosenPlan?.lowercased()
1697
+ // }
1698
+
1699
+ // Add these if recurring is enabled
1700
+ if let req = request, req.is_recurring == true {
1701
+ if let startDateText = startDate, !startDateText.isEmpty {
1702
+ let inputFormatter = DateFormatter()
1703
+ inputFormatter.dateFormat = "dd/MM/yyyy"
1704
+
1705
+ let outputFormatter = DateFormatter()
1706
+ outputFormatter.dateFormat = "MM/dd/yyyy"
1707
+
1708
+ if let date = inputFormatter.date(from: startDateText) {
1709
+ let apiFormattedDate = outputFormatter.string(from: date)
1710
+ params["start_date"] = apiFormattedDate
1711
+ } else {
1712
+ }
1713
+ }
1714
+
1715
+ // interval is still required
1716
+ params["interval"] = chosenPlan?.lowercased()
1717
+ }
1718
+
1719
+ // ✅ Include metadata only if it has at least 1 key-value pair
1720
+ if let metadata = request?.metadata, !metadata.isEmpty {
1721
+ params["metadata"] = metadata
1722
+ }
1723
+
1724
+
1725
+ do {
1726
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
1727
+ uRLRequest.httpBody = jsonData
1728
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1729
+ }
1730
+ } catch let error {
1731
+ hideLoadingIndicator()
1732
+ return
1733
+ }
1734
+
1735
+ let session = URLSession.shared
1736
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
1737
+
1738
+ DispatchQueue.main.async {
1739
+ self.hideLoadingIndicator() // Stop loader when response is received
1740
+ }
1741
+
1742
+ if let error = error {
1743
+ return
1744
+ }
1745
+
1746
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
1747
+ return
1748
+ }
1749
+
1750
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
1751
+ if let data = serviceData {
1752
+ do {
1753
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
1754
+
1755
+ // ✅ Handle duplicate transaction case
1756
+ if let status = responseObject["status"] as? Bool, status == false,
1757
+ let message = responseObject["message"] as? String,
1758
+ message.lowercased().contains("duplicate transaction") {
1759
+ self.presentPaymentErrorVC(errorMessage: message)
1760
+ return
1761
+ }
1762
+
1763
+ if let status = responseObject["status"] as? Int, status == 0,
1764
+ let message = responseObject["message"] as? String,
1765
+ message.lowercased().contains("duplicate transaction") {
1766
+ self.presentPaymentErrorVC(errorMessage: message)
1767
+ return
1768
+ }
1769
+
1770
+ // ✅ Handle generic "status == 0" error case
1771
+ if let status = responseObject["status"] as? Int, status == 0 {
1772
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
1773
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
1774
+ return
1775
+ }
1776
+ else {
1777
+ DispatchQueue.main.async {
1778
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "ThreeDSecurePaymentDoneVC") as? ThreeDSecurePaymentDoneVC {
1779
+
1780
+ let urlString = responseObject["redirect_url"] as? String ?? responseObject["location_url"] as? String ?? ""
1781
+ paymentDoneVC.redirectURL = urlString
1782
+ paymentDoneVC.chargeData = responseObject
1783
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
1784
+ paymentDoneVC.cardApiParams = params
1785
+ paymentDoneVC.request = self.request
1786
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
1787
+ }
1788
+ }
1789
+ }
1790
+ } else {
1791
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
1792
+ }
1793
+ } catch let jsonError {
1794
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
1795
+ }
1796
+ } else {
1797
+ self.presentPaymentErrorVC(errorMessage: "No data received")
1798
+ }
1799
+ } else {
1800
+ if let data = serviceData,
1801
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
1802
+ let message = responseObj["message"] as? String {
1803
+ self.presentPaymentErrorVC(errorMessage: message)
1804
+ } else {
1805
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
1806
+ }
1807
+ }
1808
+ }
1809
+ task.resume()
1810
+ }
1811
+
1812
+ // MARK: - Credit Card Charge Api If Billing info is not nil and Without Login.
1813
+ func threeDSecurePaymentSavedCardApi(customerId: String?) {
1814
+ showLoadingIndicator()
1815
+
1816
+ let fullURL = EnvironmentConfig.baseURL + EnvironmentConfig.Endpoints.threeDSecure.path()
1817
+
1818
+ guard let serviceURL = URL(string: fullURL) else {
1819
+ hideLoadingIndicator()
1820
+ return
1821
+ }
1822
+
1823
+ var uRLRequest = URLRequest(url: serviceURL)
1824
+ uRLRequest.httpMethod = "POST"
1825
+ uRLRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
1826
+
1827
+ let token = UserStoreSingleton.shared.clientToken
1828
+ uRLRequest.addValue(token ?? "", forHTTPHeaderField: "Client-Token")
1829
+
1830
+ var params: [String: Any] = [
1831
+ "name": nameOnCard ?? "",
1832
+ "email": email ?? "",
1833
+ "card_number": cardNumber?.replacingOccurrences(of: " ", with: "") ?? "",
1834
+ "cardholder_name": nameOnCard ?? "",
1835
+ "exp_month": expiryDate?.components(separatedBy: "/").first ?? "",
1836
+ "exp_year": expiryDate?.components(separatedBy: "/").last ?? "",
1837
+ "cvc": cvv ?? "",
1838
+ "currency": "usd",
1839
+ "tokenize": request.tokenOnly ?? false,
1840
+ "save_card": isSavedNewCard ? 1 : 0,
1841
+ "customer_id": customerId ?? "",
1842
+ "customer" : customerId ?? ""
1843
+ ]
1844
+
1845
+ // Add is_default parameter if save_card is 1
1846
+ if isSavedNewCard {
1847
+ params["is_default"] = "1"
1848
+ }
1849
+
1850
+ if customerId == nil {
1851
+ params["create_customer"] = "1"
1852
+ }
1853
+
1854
+ // Always include Billing Info if available
1855
+ if let billing = billingInfo, !billing.isEmpty {
1856
+ var billingDict: [String: Any] = [:]
1857
+ billing.forEach { billingDict[$0.name] = $0.value }
1858
+
1859
+ params["address"] = billingDict["address"] as? String ?? ""
1860
+ params["country"] = billingDict["country"] as? String ?? ""
1861
+ params["state"] = billingDict["state"] as? String ?? ""
1862
+ params["city"] = billingDict["city"] as? String ?? ""
1863
+ params["zip"] = billingDict["postal_code"] as? String ?? ""
1864
+ }
1865
+
1866
+ // Always include Additional Info if available
1867
+ var descriptionValue: String = "Hosted payment checkout"
1868
+ if let additional = additionalInfo, !additional.isEmpty {
1869
+ var additionalDict: [String: Any] = [:]
1870
+ additional.forEach { additionalDict[$0.name] = $0.value }
1871
+
1872
+ if let desc = additionalDict["description"] as? String, !desc.isEmpty {
1873
+ descriptionValue = desc
1874
+ }
1875
+
1876
+ if let phone = additionalDict["phone_number"] as? String, !phone.isEmpty {
1877
+ params["phone_number"] = phone
1878
+ }
1879
+ }
1880
+ params["description"] = descriptionValue
1881
+
1882
+ // Add these if recurring is enabled
1883
+ // if let req = request, req.is_recurring == true {
1884
+ // if let recurringType = req.recurringStartDateType, recurringType == .custom {
1885
+ // // Only send start_date if type is .custom and field is not empty
1886
+ // if let startDateText = startDate, !startDateText.isEmpty {
1887
+ // let inputFormatter = DateFormatter()
1888
+ // inputFormatter.dateFormat = "dd/MM/yyyy"
1889
+ //
1890
+ // let outputFormatter = DateFormatter()
1891
+ // outputFormatter.dateFormat = "MM/dd/yyyy"
1892
+ //
1893
+ // if let date = inputFormatter.date(from: startDateText) {
1894
+ // let apiFormattedDate = outputFormatter.string(from: date)
1895
+ // params["start_date"] = apiFormattedDate
1896
+ // } else {
1897
+ // }
1898
+ // }
1899
+ // }
1900
+ //
1901
+ // params["interval"] = chosenPlan?.lowercased()
1902
+ // }
1903
+
1904
+ // Add these if recurring is enabled
1905
+ if let req = request, req.is_recurring == true {
1906
+ if let startDateText = startDate, !startDateText.isEmpty {
1907
+ let inputFormatter = DateFormatter()
1908
+ inputFormatter.dateFormat = "dd/MM/yyyy"
1909
+
1910
+ let outputFormatter = DateFormatter()
1911
+ outputFormatter.dateFormat = "MM/dd/yyyy"
1912
+
1913
+ if let date = inputFormatter.date(from: startDateText) {
1914
+ let apiFormattedDate = outputFormatter.string(from: date)
1915
+ params["start_date"] = apiFormattedDate
1916
+ } else {
1917
+ }
1918
+ }
1919
+
1920
+ // interval is still required
1921
+ params["interval"] = chosenPlan?.lowercased()
1922
+ }
1923
+
1924
+ // ✅ Include metadata only if it has at least 1 key-value pair
1925
+ if let metadata = request?.metadata, !metadata.isEmpty {
1926
+ params["metadata"] = metadata
1927
+ }
1928
+
1929
+
1930
+ do {
1931
+ let jsonData = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
1932
+ uRLRequest.httpBody = jsonData
1933
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1934
+ }
1935
+ } catch let error {
1936
+ hideLoadingIndicator()
1937
+ return
1938
+ }
1939
+
1940
+ let session = URLSession.shared
1941
+ let task = session.dataTask(with: uRLRequest) { (serviceData, serviceResponse, error) in
1942
+
1943
+ DispatchQueue.main.async {
1944
+ self.hideLoadingIndicator() // Stop loader when response is received
1945
+ }
1946
+
1947
+ if let error = error {
1948
+ return
1949
+ }
1950
+
1951
+ guard let httpResponse = serviceResponse as? HTTPURLResponse else {
1952
+ return
1953
+ }
1954
+
1955
+ if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 {
1956
+ if let data = serviceData {
1957
+ do {
1958
+ if let responseObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
1959
+
1960
+ // ✅ Handle duplicate transaction case
1961
+ if let status = responseObject["status"] as? Bool, status == false,
1962
+ let message = responseObject["message"] as? String,
1963
+ message.lowercased().contains("duplicate transaction") {
1964
+ self.presentPaymentErrorVC(errorMessage: message)
1965
+ return
1966
+ }
1967
+
1968
+ if let status = responseObject["status"] as? Int, status == 0,
1969
+ let message = responseObject["message"] as? String,
1970
+ message.lowercased().contains("duplicate transaction") {
1971
+ self.presentPaymentErrorVC(errorMessage: message)
1972
+ return
1973
+ }
1974
+
1975
+ // ✅ Handle generic "status == 0" error case
1976
+ if let status = responseObject["status"] as? Int, status == 0 {
1977
+ let errorMessage = responseObject["message"] as? String ?? "Unknown error"
1978
+ self.presentPaymentErrorVC(errorMessage: errorMessage)
1979
+ return
1980
+ }
1981
+ else {
1982
+ DispatchQueue.main.async {
1983
+ if let paymentDoneVC = self.storyboard?.instantiateViewController(withIdentifier: "ThreeDSecurePaymentDoneVC") as? ThreeDSecurePaymentDoneVC {
1984
+
1985
+ let urlString = responseObject["redirect_url"] as? String ?? responseObject["location_url"] as? String ?? ""
1986
+ paymentDoneVC.redirectURL = urlString
1987
+ paymentDoneVC.chargeData = responseObject
1988
+ paymentDoneVC.easyPayDelegate = self.easyPayDelegate
1989
+ // Pass billing and additional info
1990
+ // Conditionally pass raw FieldItem array
1991
+ paymentDoneVC.visibility = self.visibility
1992
+ paymentDoneVC.amount = self.amount
1993
+ paymentDoneVC.cardApiParams = params
1994
+ paymentDoneVC.request = self.request
1995
+
1996
+ // if self.visibility?.billing == true {
1997
+ paymentDoneVC.billingInfoData = self.billingInfo
1998
+ var billingDict: [String: Any] = [:]
1999
+ self.billingInfo?.forEach { billingDict[$0.name] = $0.value }
2000
+ paymentDoneVC.billingInfo = billingDict
2001
+ // }
2002
+
2003
+ // if self.visibility?.additional == true {
2004
+ paymentDoneVC.additionalInfoData = self.additionalInfo
2005
+ var additionalDict: [String: Any] = [:]
2006
+ self.additionalInfo?.forEach { additionalDict[$0.name] = $0.value }
2007
+ paymentDoneVC.additionalInfo = additionalDict
2008
+ // }
2009
+ self.navigationController?.pushViewController(paymentDoneVC, animated: true)
2010
+ }
2011
+ }
2012
+ }
2013
+ } else {
2014
+ self.presentPaymentErrorVC(errorMessage: "Invalid JSON format")
2015
+ }
2016
+ } catch let jsonError {
2017
+ self.presentPaymentErrorVC(errorMessage: "Error parsing JSON: \(jsonError)")
2018
+ }
2019
+ } else {
2020
+ self.presentPaymentErrorVC(errorMessage: "No data received")
2021
+ }
2022
+ } else {
2023
+ if let data = serviceData,
2024
+ let responseObj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
2025
+ let message = responseObj["message"] as? String {
2026
+ self.presentPaymentErrorVC(errorMessage: message)
2027
+ } else {
2028
+ self.presentPaymentErrorVC(errorMessage: "HTTP Status Code: \(httpResponse.statusCode)")
2029
+ }
2030
+ }
2031
+ }
2032
+ task.resume()
2033
+ }
2034
+
2035
+ @IBAction func actionBtnCancel(_ sender: UIButton) {
2036
+ stopTimer()
2037
+ for viewController in self.navigationController?.viewControllers ?? [] {
2038
+ if viewController is PaymentInfoVC {
2039
+ self.navigationController?.popToViewController(viewController, animated: true)
2040
+ break
2041
+ }
2042
+ }
2043
+ }
2044
+
2045
+ @IBAction func actionBtnResendOTP(_ sender: UIButton) {
2046
+ startTimer()
2047
+ btnResendOTP.isHidden = true
2048
+ imgEsclamationMark.isHidden = false
2049
+ lblOtpTimer.isHidden = false
2050
+ lblUntillResendOtp.isHidden = false
2051
+
2052
+ // Call email verification APi
2053
+ emailVerificationApi()
2054
+ }
2055
+
2056
+ @IBAction func actionBtnConfirmCode(_ sender: UIButton) {
2057
+ otpVerificationApi()
2058
+ }
2059
+
2060
+ }
2061
+
2062
+ extension OTPVerificationVC: UITextFieldDelegate {
2063
+
2064
+ func textFieldDidChangeSelection(_ textField: UITextField) {
2065
+ guard let otpCode = textField.text, otpCode.count >= 6, textField.textContentType == .oneTimeCode else { return }
2066
+ txtFieldOTPText1.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 0)])
2067
+ txtFieldOTPText2.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 1)])
2068
+ txtFieldOTPText3.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 2)])
2069
+ txtFieldOTPText4.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 3)])
2070
+ txtFieldOTPText5.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 4)])
2071
+ txtFieldOTPText6.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 5)])
2072
+ txtFieldOTPText6.becomeFirstResponder()
2073
+ }
2074
+
2075
+ func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
2076
+ if [txtFieldOTPText1, txtFieldOTPText2, txtFieldOTPText3, txtFieldOTPText4, txtFieldOTPText5, txtFieldOTPText6].contains(textField) {
2077
+ if string.count == 1 {
2078
+ // Single character input
2079
+ textField.text = string
2080
+ switch textField {
2081
+ case txtFieldOTPText1:
2082
+ txtFieldOTPText2.becomeFirstResponder()
2083
+ case txtFieldOTPText2:
2084
+ txtFieldOTPText3.becomeFirstResponder()
2085
+ case txtFieldOTPText3:
2086
+ txtFieldOTPText4.becomeFirstResponder()
2087
+ case txtFieldOTPText4:
2088
+ txtFieldOTPText5.becomeFirstResponder()
2089
+ case txtFieldOTPText5:
2090
+ txtFieldOTPText6.becomeFirstResponder()
2091
+ case txtFieldOTPText6:
2092
+ txtFieldOTPText6.resignFirstResponder()
2093
+ // Call OTP verification API when the last field is filled
2094
+ if getCombinedOTP().count == 6 {
2095
+ // otpVerificationApi()
2096
+ }
2097
+ default:
2098
+ break
2099
+ }
2100
+ updateViewColors()
2101
+ return false
2102
+ } else if string.isEmpty {
2103
+ // Handle backspace
2104
+ textField.text = ""
2105
+ switch textField {
2106
+ case txtFieldOTPText6:
2107
+ txtFieldOTPText5.becomeFirstResponder()
2108
+ case txtFieldOTPText5:
2109
+ txtFieldOTPText4.becomeFirstResponder()
2110
+ case txtFieldOTPText4:
2111
+ txtFieldOTPText3.becomeFirstResponder()
2112
+ case txtFieldOTPText3:
2113
+ txtFieldOTPText2.becomeFirstResponder()
2114
+ case txtFieldOTPText2:
2115
+ txtFieldOTPText1.becomeFirstResponder()
2116
+ default:
2117
+ break
2118
+ }
2119
+ updateViewColors()
2120
+ return false
2121
+ }
2122
+ return textField.text?.count == 0
2123
+ }
2124
+
2125
+ return true
2126
+ }
2127
+
2128
+ // Update the view colors based on text presence in OTP fields
2129
+ private func updateViewColors() {
2130
+ if let primaryBtnBackgroundColor = UserStoreSingleton.shared.primary_btn_bg_col,
2131
+ let primaryUIColor = UIColor(hex: primaryBtnBackgroundColor),
2132
+ let secondaryFontColor = UserStoreSingleton.shared.secondary_font_col,
2133
+ let secondaryUIColor = UIColor(hex: secondaryFontColor) {
2134
+
2135
+ viewTextOTP1.layer.borderWidth = 1.0
2136
+ viewTextOTP1.layer.borderColor = txtFieldOTPText1.text?.isEmpty == false ? primaryUIColor.cgColor : secondaryUIColor.cgColor
2137
+ viewTextOTP2.layer.borderWidth = 1.0
2138
+ viewTextOTP2.layer.borderColor = txtFieldOTPText2.text?.isEmpty == false ? primaryUIColor.cgColor : secondaryUIColor.cgColor
2139
+ viewTextOTP3.layer.borderWidth = 1.0
2140
+ viewTextOTP3.layer.borderColor = txtFieldOTPText3.text?.isEmpty == false ? primaryUIColor.cgColor : secondaryUIColor.cgColor
2141
+ viewTextOTP4.layer.borderWidth = 1.0
2142
+ viewTextOTP4.layer.borderColor = txtFieldOTPText4.text?.isEmpty == false ? primaryUIColor.cgColor : secondaryUIColor.cgColor
2143
+ viewTextOTP5.layer.borderWidth = 1.0
2144
+ viewTextOTP5.layer.borderColor = txtFieldOTPText5.text?.isEmpty == false ? primaryUIColor.cgColor : secondaryUIColor.cgColor
2145
+ viewTextOTP6.layer.borderWidth = 1.0
2146
+ viewTextOTP6.layer.borderColor = txtFieldOTPText6.text?.isEmpty == false ? primaryUIColor.cgColor : secondaryUIColor.cgColor
2147
+ }
2148
+ else {
2149
+ viewTextOTP1.layer.borderWidth = 1.0
2150
+ viewTextOTP1.layer.borderColor = (txtFieldOTPText1.text?.isEmpty == false ? UIColor.systemBlue : UIColor.systemGray).cgColor
2151
+ viewTextOTP2.layer.borderWidth = 1.0
2152
+ viewTextOTP2.layer.borderColor = (txtFieldOTPText2.text?.isEmpty == false ? UIColor.systemBlue : UIColor.systemGray).cgColor
2153
+ viewTextOTP3.layer.borderWidth = 1.0
2154
+ viewTextOTP3.layer.borderColor = (txtFieldOTPText3.text?.isEmpty == false ? UIColor.systemBlue : UIColor.systemGray).cgColor
2155
+ viewTextOTP4.layer.borderWidth = 1.0
2156
+ viewTextOTP4.layer.borderColor = (txtFieldOTPText4.text?.isEmpty == false ? UIColor.systemBlue : UIColor.systemGray).cgColor
2157
+ viewTextOTP5.layer.borderWidth = 1.0
2158
+ viewTextOTP5.layer.borderColor = (txtFieldOTPText5.text?.isEmpty == false ? UIColor.systemBlue : UIColor.systemGray).cgColor
2159
+ viewTextOTP6.layer.borderWidth = 1.0
2160
+ viewTextOTP6.layer.borderColor = (txtFieldOTPText6.text?.isEmpty == false ? UIColor.systemBlue : UIColor.systemGray).cgColor
2161
+ }
2162
+ }
2163
+
2164
+ @objc func textFieldDidChange(_ textField: UITextField) {
2165
+ guard let otpCode = textField.text, otpCode.count >= 6, textField.textContentType == .oneTimeCode else { return }
2166
+ // Split the OTP into each text field
2167
+ txtFieldOTPText1.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 0)])
2168
+ txtFieldOTPText2.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 1)])
2169
+ txtFieldOTPText3.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 2)])
2170
+ txtFieldOTPText4.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 3)])
2171
+ txtFieldOTPText5.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 4)])
2172
+ txtFieldOTPText6.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 5)])
2173
+ // Automatically move to the last text field
2174
+ txtFieldOTPText6.becomeFirstResponder()
2175
+ }
2176
+
2177
+ func getCombinedOTP() -> String {
2178
+ let otp1 = txtFieldOTPText1.text ?? ""
2179
+ let otp2 = txtFieldOTPText2.text ?? ""
2180
+ let otp3 = txtFieldOTPText3.text ?? ""
2181
+ let otp4 = txtFieldOTPText4.text ?? ""
2182
+ let otp5 = txtFieldOTPText5.text ?? ""
2183
+ let otp6 = txtFieldOTPText6.text ?? ""
2184
+
2185
+ return otp1 + otp2 + otp3 + otp4 + otp5 + otp6
2186
+ }
2187
+
2188
+ }
2189
+
2190
+
2191
+
2192
+
2193
+