@jimrising/easymerchantsdk-react-native 1.3.8 → 1.4.1

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 (35) hide show
  1. package/.idea/caches/deviceStreaming.xml +714 -0
  2. package/.idea/em-MobileCheckoutSDK-ReactNative.iml +9 -0
  3. package/.idea/misc.xml +6 -0
  4. package/.idea/modules.xml +8 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/README.md +140 -81
  7. package/android/.gradle/8.10/checksums/checksums.lock +0 -0
  8. package/android/.gradle/8.10/checksums/md5-checksums.bin +0 -0
  9. package/android/.gradle/8.10/checksums/sha1-checksums.bin +0 -0
  10. package/android/.gradle/8.10/fileHashes/fileHashes.lock +0 -0
  11. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  12. package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
  13. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  14. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  15. package/android/.gradle/buildOutputCleanup/cache.properties +2 -2
  16. package/ios/Classes/EasyMerchantSdk.m +106 -55
  17. package/ios/Classes/EasyMerchantSdk.swift +232 -117
  18. package/ios/Classes/EasyPayViewController.swift +1 -1
  19. package/ios/CustomComponents/DatePickerHandler.swift +15 -4
  20. package/ios/EnvironmentConfig.swift +32 -30
  21. package/ios/Models/Request.swift +176 -14
  22. package/ios/Models/Result.swift +15 -35
  23. package/ios/Pods/ViewControllers/AdditionalInfoVC.swift +851 -342
  24. package/ios/Pods/ViewControllers/BaseVC.swift +39 -35
  25. package/ios/Pods/ViewControllers/BillingInfoVC/BillingInfoVC.swift +1975 -170
  26. package/ios/Pods/ViewControllers/CountryListVC.swift +0 -1
  27. package/ios/Pods/ViewControllers/EmailVerificationVC.swift +74 -5
  28. package/ios/Pods/ViewControllers/GrailPayVC.swift +131 -107
  29. package/ios/Pods/ViewControllers/OTPVerificationVC.swift +305 -107
  30. package/ios/Pods/ViewControllers/PaymentDoneVC.swift +61 -14
  31. package/ios/Pods/ViewControllers/PaymentInformation/PaymentInfoVC.swift +1277 -501
  32. package/ios/Pods/ViewControllers/ThreeDSecurePaymentDoneVC.swift +133 -2
  33. package/ios/easymerchantsdk.podspec +1 -1
  34. package/ios/easymerchantsdk.storyboard +713 -590
  35. package/package.json +2 -2
@@ -1,51 +1,67 @@
1
1
  import Foundation
2
2
  import UIKit
3
3
  import React
4
+ //import EasyPay // Import the EasyPay module
4
5
 
5
6
  @objc(EasyMerchantSdkPlugin)
6
7
  public class EasyMerchantSdkPlugin: NSObject, RCTBridgeModule {
7
8
 
8
9
  // MARK: - React Native Module Setup
9
- public static func moduleName() -> String { "EasyMerchantSdk" }
10
- public static func requiresMainQueueSetup() -> Bool { true }
10
+ public static func moduleName() -> String! {
11
+ return "EasyMerchantSdk"
12
+ }
13
+
14
+ public static func requiresMainQueueSetup() -> Bool {
15
+ return true
16
+ }
11
17
 
12
18
  public var bridge: RCTBridge?
13
19
 
14
- // MARK: - Private Properties
15
- private var viewController: UIViewController?
16
- private let viewControllerQueue = DispatchQueue(label: "com.easymerchantsdk.viewController")
17
-
20
+ // MARK: - Stored Promise Callbacks
18
21
  private var billingResolver: RCTPromiseResolveBlock?
19
22
  private var billingRejecter: RCTPromiseRejectBlock?
23
+ private var paymentReferenceResolver: RCTPromiseResolveBlock?
24
+ private var paymentReferenceRejecter: RCTPromiseRejectBlock?
20
25
 
21
- // MARK: - Initializer
26
+ // MARK: - View Controller Reference
27
+ private let viewControllerQueue = DispatchQueue(label: "com.easymerchantsdk.viewController")
28
+ private var viewController: UIViewController?
29
+
30
+ // MARK: - Init
22
31
  @objc public override init() {
23
32
  super.init()
24
33
  }
25
34
 
26
- // Called when the bridge finishes loading
27
35
  public func bridgeDidFinishLoading() {
28
- if self.bridge != nil {
29
- print("Bridge has been initialized successfully.")
30
- } else {
31
- print("Bridge initialization failed.")
36
+ if bridge != nil {
37
+ print(" EasyMerchantSdkPlugin bridge initialized")
32
38
  }
33
39
  }
34
40
 
35
- // MARK: - Set ViewController (for presenting modal)
36
- @objc public func setViewController(_ viewController: UIViewController) {
41
+ // Expose setter for RN to inject the host view controller
42
+ @objc public func setViewController(_ vc: UIViewController) {
37
43
  viewControllerQueue.sync {
38
- self.viewController = viewController
44
+ self.viewController = vc
39
45
  }
40
- print("ViewController set: \(String(describing: self.viewController))")
41
46
  }
42
47
 
43
- // MARK: - Billing method exposed to React Native
48
+ // MARK: - configureEnvironment(...) Exposed to RN
49
+ @objc public func configureEnvironment(_ env: String, apiKey: String, apiSecret: String) {
50
+ switch env.lowercased() {
51
+ case "production": EnvironmentConfig.setEnvironment(.production)
52
+ case "sandbox": EnvironmentConfig.setEnvironment(.sandbox)
53
+ default: EnvironmentConfig.setEnvironment(.staging)
54
+ }
55
+ EnvironmentConfig.configure(apiKey: apiKey, apiSecret: apiSecret)
56
+ }
57
+
58
+ // MARK: - billing(...) Exposed to RN
44
59
  @objc public func billing(
45
60
  _ amount: String,
46
- billinginfo: String?,
61
+ currency: String?,
62
+ billingInfo: [String: Any]?,
47
63
  paymentMethods: [String],
48
- themeConfiguration: [String: Any],
64
+ themeConfiguration: [String: Any]?,
49
65
  tokenOnly: Bool,
50
66
  saveCard: Bool,
51
67
  saveAccount: Bool,
@@ -56,187 +72,286 @@ public class EasyMerchantSdkPlugin: NSObject, RCTBridgeModule {
56
72
  recurringIntervals: [String]?,
57
73
  recurringStartDateType: String?,
58
74
  recurringStartDate: String?,
75
+ secureAuthentication: Bool,
76
+ showReceipt: Bool,
77
+ showTotal: Bool,
78
+ showSubmitButton: Bool,
59
79
  enable3DS: Bool,
60
80
  resolver: @escaping RCTPromiseResolveBlock,
61
81
  rejecter: @escaping RCTPromiseRejectBlock
62
82
  ) {
63
- // Validate amount
64
- guard let amountValue = Double(amount), amountValue > 0 else {
65
- rejecter("INVALID_AMOUNT", "Amount must be a positive number", nil)
83
+ // 1) Validate amount
84
+ guard let amountValue = Double(amount), amountValue >= 0.50 else {
85
+ rejecter("INVALID_AMOUNT", "Amount must be at least $0.50", nil)
66
86
  return
67
87
  }
68
88
 
69
- // Validate billinginfo JSON if present
70
- if let billinginfo = billinginfo {
71
- guard let billingInfoData = billinginfo.data(using: .utf8),
72
- (try? JSONSerialization.jsonObject(with: billingInfoData, options: [])) != nil else {
73
- rejecter("INVALID_BILLING_INFO", "Billing info must be valid JSON", nil)
89
+ // 2) Process billing info into FieldSection
90
+ var billingInfoData: Data? = nil
91
+ if let billingDict = billingInfo {
92
+ var billingFields: [FieldItem] = []
93
+ var additionalFields: [FieldItem] = []
94
+ var billingVisibility = true
95
+ var additionalVisibility = true
96
+
97
+ // Parse billing fields
98
+ if let billing = billingDict["billing"] as? [String: Any] {
99
+ for (key, value) in billing {
100
+ guard let fieldValue = value as? String else { continue }
101
+ let required = (billingDict["billingRequired"] as? [String: Bool])?[key] ?? false
102
+ if let billingField = BillingFieldName(rawValue: key) {
103
+ billingFields.append(FieldItem(name: billingField, required: required, value: fieldValue))
104
+ }
105
+ }
106
+ }
107
+
108
+ // Parse additional fields
109
+ if let additional = billingDict["additional"] as? [String: Any] {
110
+ for (key, value) in additional {
111
+ guard let fieldValue = value as? String else { continue }
112
+ let required = (billingDict["additionalRequired"] as? [String: Bool])?[key] ?? false
113
+ if let additionalField = AdditionalFieldName(rawValue: key) {
114
+ additionalFields.append(FieldItem(name: additionalField, required: required, value: fieldValue))
115
+ }
116
+ }
117
+ }
118
+
119
+ // Parse visibility
120
+ if let visibility = billingDict["visibility"] as? [String: Bool] {
121
+ billingVisibility = visibility["billing"] ?? true
122
+ additionalVisibility = visibility["additional"] ?? true
123
+ }
124
+
125
+ let fields = FieldSection(
126
+ visibility: FieldsVisibility(billing: billingVisibility, additional: additionalVisibility),
127
+ billing: billingFields,
128
+ additional: additionalFields
129
+ )
130
+
131
+ do {
132
+ billingInfoData = try JSONEncoder().encode(fields)
133
+ } catch {
134
+ rejecter("INVALID_BILLING_INFO", "Failed to encode billing info: \(error.localizedDescription)", nil)
74
135
  return
75
136
  }
76
137
  }
77
138
 
78
- // Map payment method strings to enum cases
139
+ // 3) Map payment methods
79
140
  let methods: [PaymentMethod] = paymentMethods.compactMap {
80
141
  switch $0.lowercased() {
81
- case "card": return .Card
82
- case "bank": return .Bank
142
+ case "card": return .Card
143
+ case "bank": return .Bank
83
144
  case "crypto": return .Crypto
84
- default: return nil
145
+ case "wallet": return .Wallet
146
+ default: return nil
85
147
  }
86
148
  }
87
149
 
88
- // Setup GrailPayRequest if authenticatedACH is true
150
+ // 4) Prepare GrailPayRequest if needed
89
151
  var grailParams: GrailPayRequest? = nil
90
152
  if authenticatedACH, let params = grailPayParams {
91
153
  grailParams = GrailPayRequest(
92
- accessToken: params["accessToken"] as? String ?? "",
93
- vendorId: params["vendorId"] as? String ?? "",
154
+ accessToken: params["accessToken"] as? String ?? "251|uTijpDGfrS88UR2V1cZNMQ8S4hUJA0sVzsnsoUZF",
155
+ vendorId: params["vendorId"] as? String ?? "251",
94
156
  role: params["role"] as? String ?? "business",
95
157
  timeout: params["timeout"] as? Int ?? 10,
96
158
  isSandbox: params["isSandbox"] as? Bool ?? true,
97
- brandingName: params["brandingName"] as? String ?? "Payments",
98
- finderSubtitle: params["finderSubtitle"] as? String ?? "Find your bank",
99
- searchPlaceholder: params["searchPlaceholder"] as? String ?? "Search"
159
+ brandingName: params["brandingName"] as? String ?? "Lyfecycle Payments",
160
+ finderSubtitle: params["finderSubtitle"] as? String ?? "Search for your bank",
161
+ searchPlaceholder: params["searchPlaceholder"] as? String ?? "Enter bank name"
100
162
  )
101
163
  }
102
164
 
103
- // Setup Theme Configuration with default fallbacks
165
+ // 5) Build theme config
104
166
  let themeConfig = ThemeConfiguration(
105
- bodyBackgroundColor: themeConfiguration["bodyBackgroundColor"] as? String ?? "#EBF8FF",
106
- containerBackgroundColor: themeConfiguration["containerBackgroundColor"] as? String ?? "#FFFFFF",
107
- primaryFontColor: themeConfiguration["primaryFontColor"] as? String ?? "#1E3A8A",
108
- secondaryFontColor: themeConfiguration["secondaryFontColor"] as? String ?? "#696969",
109
- primaryButtonBackgroundColor: themeConfiguration["primaryButtonBackgroundColor"] as? String ?? "#1D4ED8",
110
- primaryButtonHoverColor: themeConfiguration["primaryButtonHoverColor"] as? String ?? "#2563EB",
111
- primaryButtonFontColor: themeConfiguration["primaryButtonFontColor"] as? String ?? "#FFFFFF",
112
- secondaryButtonBackgroundColor: themeConfiguration["secondaryButtonBackgroundColor"] as? String ?? "#FFFFFF",
113
- secondaryButtonHoverColor: themeConfiguration["secondaryButtonHoverColor"] as? String ?? "#BFDBFE",
114
- secondaryButtonFontColor: themeConfiguration["secondaryButtonFontColor"] as? String ?? "#1E40AF",
115
- borderRadius: themeConfiguration["borderRadius"] as? String ?? "8",
116
- fontSize: themeConfiguration["fontSize"] as? String ?? "14"
167
+ bodyBackgroundColor: themeConfiguration?["bodyBackgroundColor"] as? String ?? "#121212",
168
+ containerBackgroundColor: themeConfiguration?["containerBackgroundColor"] as? String ?? "#1E1E1E",
169
+ primaryFontColor: themeConfiguration?["primaryFontColor"] as? String ?? "#FFFFFF",
170
+ secondaryFontColor: themeConfiguration?["secondaryFontColor"] as? String ?? "#B0B0B0",
171
+ primaryButtonBackgroundColor: themeConfiguration?["primaryButtonBackgroundColor"] as? String ?? "#2563EB",
172
+ primaryButtonHoverColor: themeConfiguration?["primaryButtonHoverColor"] as? String ?? "#1D4ED8",
173
+ primaryButtonFontColor: themeConfiguration?["primaryButtonFontColor"] as? String ?? "#FFFFFF",
174
+ secondaryButtonBackgroundColor: themeConfiguration?["secondaryButtonBackgroundColor"] as? String ?? "#374151",
175
+ secondaryButtonHoverColor: themeConfiguration?["secondaryButtonHoverColor"] as? String ?? "#4B5563",
176
+ secondaryButtonFontColor: themeConfiguration?["secondaryButtonFontColor"] as? String ?? "#E5E7EB",
177
+ borderRadius: themeConfiguration?["borderRadius"] as? String ?? "8",
178
+ fontSize: themeConfiguration?["fontSize"] as? String ?? "14",
179
+ fontWeight: themeConfiguration?["fontWeight"] as? Int ?? 500,
180
+ fontFamily: themeConfiguration?["fontFamily"] as? String ?? "\"Inter\", sans-serif"
117
181
  )
118
182
 
119
- // Prepare billing info data
120
- let billingInfoData = billinginfo?.data(using: .utf8) ?? Data()
121
-
122
-
123
- print("🔥 recurringIntervals raw from JS:", recurringIntervals as Any)
124
-
125
- // Updated mapping to strip leading "." if present
126
- let intervals = (recurringIntervals ?? []).compactMap { raw -> RecurringIntervals? in
127
- // 1) Trim whitespace, lowercase
183
+ // 6) Map recurring intervals
184
+ let intervals: [RecurringIntervals] = (recurringIntervals ?? []).compactMap { raw in
128
185
  let cleaned = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
129
- // 2) If it starts with ".", drop that character
130
186
  let noDot = cleaned.hasPrefix(".") ? String(cleaned.dropFirst()) : cleaned
131
- // 3) Try to initialize your enum
132
187
  return RecurringIntervals(rawValue: noDot)
133
188
  }
134
189
 
135
- print("🔥 mapped intervals after stripping dots:", intervals)
136
-
137
- guard !intervals.isEmpty else {
190
+ if isRecurring && intervals.isEmpty {
138
191
  rejecter("NO_RECURRING_INTERVALS", "No recurring intervals available.", nil)
139
192
  return
140
- }
141
-
193
+ }
142
194
 
143
- // 2) Map recurringStartDateType (assuming you have an enum for it)
144
- let recurringStartDateTypeEnum: RecurringStartDateType = {
145
- switch recurringStartDateType?.lowercased() {
146
- case "custom": return .custom
147
- case "fixed": return .fixed
148
- default: return .custom
149
- }
150
- }()
195
+ // 7) Map start date type
196
+ let startType: RecurringStartDateType? = {
197
+ switch recurringStartDateType?.lowercased() {
198
+ case "custom": return .custom
199
+ case "fixed": return .fixed
200
+ default: return nil
201
+ }
202
+ }()
151
203
 
152
- // Create Request object with all parameters
204
+ // 8) Build the request object
153
205
  let request = Request(
154
206
  amount: amountValue,
207
+ currency: currency ?? "usd",
155
208
  billingInfoData: billingInfoData,
156
- paymentMethods: methods,
209
+ paymentMethods: methods.isEmpty ? [.Card, .Bank] : methods,
157
210
  themeConfiguration: themeConfig,
158
211
  tokenOnly: tokenOnly,
159
212
  saveCard: saveCard,
160
213
  saveAccount: saveAccount,
161
214
  submitButtonText: submitButtonText ?? "Submit",
162
215
  authenticatedACH: authenticatedACH,
163
- grailPayParams: authenticatedACH ? grailParams : nil,
216
+ grailPayParams: grailParams,
164
217
  is_recurring: isRecurring,
165
218
  recurringIntervals: intervals,
166
- recurringStartDateType: recurringStartDateTypeEnum,
219
+ recurringStartDateType: startType,
167
220
  recurringStartDate: recurringStartDate,
168
- enable3DS: enable3DS
221
+ secureAuthentication: secureAuthentication,
222
+ showReceipt: showReceipt,
223
+ showTotal: showTotal,
224
+ showSubmitButton: showSubmitButton,
225
+ referenceID: enable3DS,
226
+ referenceToken: nil
169
227
  )
170
228
 
171
- // Store resolve/reject callbacks for later
229
+ // Store resolvers
172
230
  self.billingResolver = resolver
173
231
  self.billingRejecter = rejecter
174
232
 
175
- DispatchQueue.main.async {
176
- let controller = EasyPayViewController(request: request, delegate: self)
177
- if let vc = self.viewController {
178
- vc.present(controller, animated: true, completion: nil)
179
- } else if let rootVC = UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController {
180
- rootVC.present(controller, animated: true, completion: nil)
181
- } else {
182
- rejecter("NO_VIEW_CONTROLLER", "No view controller available to present payment screen.", nil)
233
+ // Handle tokenOnly or present EasyPayViewController
234
+ if tokenOnly {
235
+ NotificationCenter.default.addObserver(
236
+ forName: NSNotification.Name("ClientTokenReceived"),
237
+ object: nil,
238
+ queue: .main
239
+ ) { [weak self] notification in
240
+ guard let self = self else { return }
241
+ if let clientToken = notification.object as? String {
242
+ self.billingResolver?(["status": "success", "clientToken": clientToken])
243
+ } else {
244
+ self.billingRejecter?("TOKEN_ERROR", "Failed to receive client token.", nil)
245
+ }
183
246
  self.clearResolvers()
184
247
  }
248
+ } else {
249
+ DispatchQueue.main.async {
250
+ let vc = EasyPayViewController(request: request, delegate: self)
251
+ if let host = self.viewController {
252
+ host.present(vc, animated: true, completion: nil)
253
+ } else if let root = UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController {
254
+ root.present(vc, animated: true, completion: nil)
255
+ } else {
256
+ rejecter("NO_VIEW_CONTROLLER", "Cannot find a view controller to present.", nil)
257
+ self.clearResolvers()
258
+ }
259
+ }
185
260
  }
186
261
  }
187
262
 
188
- // MARK: - Environment Configuration
189
- @objc public func configureEnvironment(_ env: String, apiKey: String, apiSecret: String) {
190
- switch env.lowercased() {
191
- case "production":
192
- EnvironmentConfig.setEnvironment(.production)
193
- case "sandbox":
194
- EnvironmentConfig.setEnvironment(.sandbox)
195
- default:
196
- EnvironmentConfig.setEnvironment(.staging)
263
+ // MARK: - paymentReference(...) Exposed to RN
264
+ @objc public func paymentReference(
265
+ _ referenceToken: String,
266
+ resolver: @escaping RCTPromiseResolveBlock,
267
+ rejecter: @escaping RCTPromiseRejectBlock
268
+ ) {
269
+ // Validate reference token
270
+ guard !referenceToken.isEmpty else {
271
+ rejecter("INVALID_REFERENCE_TOKEN", "Reference token cannot be empty.", nil)
272
+ return
273
+ }
274
+
275
+ // Store resolvers
276
+ self.paymentReferenceResolver = resolver
277
+ self.paymentReferenceRejecter = rejecter
278
+
279
+ // Build the request object
280
+ let request = Request(
281
+ referenceID: true,
282
+ referenceToken: referenceToken
283
+ )
284
+
285
+ // Set up notification observer for ReferenceIDResponse
286
+ DispatchQueue.main.async {
287
+ NotificationCenter.default.addObserver(
288
+ forName: NSNotification.Name("ReferenceIDResponse"),
289
+ object: nil,
290
+ queue: .main
291
+ ) { [weak self] notification in
292
+ guard let self = self else { return }
293
+ if let data = notification.object as? [String: Any] {
294
+ self.paymentReferenceResolver?(data)
295
+ } else {
296
+ self.paymentReferenceRejecter?("INVALID_RESPONSE", "Invalid response data.", nil)
297
+ }
298
+ self.clearReferenceResolvers()
299
+ }
197
300
  }
198
- EnvironmentConfig.configure(apiKey: apiKey, apiSecret: apiSecret)
199
301
  }
200
302
 
201
- // MARK: - Platform Version
303
+ // MARK: - getPlatformVersion(...) Exposed to RN
202
304
  @objc public func getPlatformVersion(
203
- _ resolver: @escaping RCTPromiseResolveBlock,
204
- rejecter: @escaping RCTPromiseRejectBlock
305
+ _ resolver: RCTPromiseResolveBlock,
306
+ rejecter: RCTPromiseRejectBlock
205
307
  ) {
206
308
  resolver("iOS \(UIDevice.current.systemVersion)")
207
309
  }
208
310
 
209
- // MARK: - Clear stored callbacks
311
+ // MARK: - Private Helpers
210
312
  private func clearResolvers() {
211
- self.billingResolver = nil
212
- self.billingRejecter = nil
313
+ billingResolver = nil
314
+ billingRejecter = nil
315
+ }
316
+
317
+ private func clearReferenceResolvers() {
318
+ paymentReferenceResolver = nil
319
+ paymentReferenceRejecter = nil
320
+ NotificationCenter.default.removeObserver(self, name: NSNotification.Name("ReferenceIDResponse"), object: nil)
213
321
  }
214
322
  }
215
323
 
216
- // MARK: - EasyPayViewControllerDelegate Implementation
324
+ // MARK: - Delegate Implementation
217
325
  extension EasyMerchantSdkPlugin: EasyPayViewControllerDelegate {
218
326
  public func easyPayController(_ controller: EasyPayViewController, didFinishWith result: Result) {
219
327
  DispatchQueue.main.async {
220
328
  switch result.type {
221
329
  case .cancelled:
222
330
  self.billingResolver?(["status": "cancelled", "message": "User cancelled"])
331
+
223
332
  case .success:
224
- if let chargeData = result.chargeData,
225
- let clientToken = chargeData["clientToken"] as? String {
226
- self.billingResolver?(["status": "success", "clientToken": clientToken])
227
- } else {
228
- self.billingResolver?(["status": "success", "chargeData": result.chargeData ?? [:]])
333
+ var payload: [String: Any] = ["status": "success"]
334
+ if let cd = result.chargeData { payload["chargeData"] = cd }
335
+ if let bi = result.billingInfo { payload["billingInfo"] = bi }
336
+ if let ai = result.additionalInfo { payload["additionalInfo"] = ai }
337
+
338
+ // Extract referenceToken for 3DS
339
+ if let additional = result.additionalInfo,
340
+ let threeDSecureStatus = additional["threeDSecureStatus"] as? [String: Any],
341
+ let data = threeDSecureStatus["data"] as? [String: Any],
342
+ let token = data["ref_token"] as? String {
343
+ payload["referenceToken"] = token
229
344
  }
345
+
346
+ self.billingResolver?(payload)
347
+
230
348
  case .error:
231
- if let error = result.chargeData?["errorMessage"] as? String {
232
- self.billingRejecter?("PAYMENT_ERROR", error, nil)
233
- } else {
234
- self.billingRejecter?("PAYMENT_ERROR", "Unknown error", nil)
235
- }
349
+ let errMsg = (result.chargeData?["errorMessage"] as? String) ?? "Unknown error"
350
+ self.billingRejecter?("PAYMENT_ERROR", errMsg, nil)
236
351
  }
352
+
237
353
  self.clearResolvers()
238
- controller.dismiss(animated: true)
354
+ controller.dismiss(animated: true, completion: nil)
239
355
  }
240
356
  }
241
357
  }
242
-
@@ -63,7 +63,7 @@ public final class EasyPayViewController: UINavigationController {
63
63
  let vc = UIStoryboard(name: "easymerchantsdk", bundle: Bundle.easyPayBundle).instantiateViewController(withIdentifier: "PaymentInfoVC") as! PaymentInfoVC
64
64
  vc.modalPresentationStyle = .overFullScreen
65
65
  vc.configureWith(request: request, delegate: easyPayDelegate)
66
- vc.amount = Int(request.amount) // Set the amount here
66
+ vc.amount = Int(request.amount ?? 0.0) // Set the amount here
67
67
  self.navigationBar.isHidden = true
68
68
  self.setViewControllers([vc], animated: false)
69
69
  }
@@ -19,12 +19,14 @@ class DatePickerHandler: NSObject {
19
19
 
20
20
  self.targetTextField = textField
21
21
  configureDatePicker()
22
+ setupInputView(for: textField)
22
23
 
23
24
  if let date = initialDate {
24
25
  datePicker.date = date
25
26
  }
26
27
 
27
- setupInputView(for: textField)
28
+ // Add observer for when the user taps the textField
29
+ textField.addTarget(self, action: #selector(textFieldEditingDidBegin), for: .editingDidBegin)
28
30
  }
29
31
 
30
32
  private func configureDatePicker() {
@@ -32,7 +34,7 @@ class DatePickerHandler: NSObject {
32
34
  datePicker.preferredDatePickerStyle = .inline
33
35
  }
34
36
  datePicker.datePickerMode = .date
35
- datePicker.minimumDate = Date()
37
+ datePicker.minimumDate = Calendar.current.startOfDay(for: Date())
36
38
  datePicker.addTarget(self, action: #selector(dateChanged(_:)), for: .valueChanged)
37
39
  }
38
40
 
@@ -49,6 +51,17 @@ class DatePickerHandler: NSObject {
49
51
  textField.inputAccessoryView = toolbar
50
52
  }
51
53
 
54
+ @objc private func textFieldEditingDidBegin() {
55
+ // When the textField is tapped, if it has a date, set it back on the date picker
56
+ if let text = targetTextField?.text, !text.isEmpty {
57
+ let formatter = DateFormatter()
58
+ formatter.dateFormat = "dd/MM/yyyy"
59
+ if let selectedDate = formatter.date(from: text) {
60
+ datePicker.setDate(selectedDate, animated: true)
61
+ }
62
+ }
63
+ }
64
+
52
65
  @objc private func dateChanged(_ sender: UIDatePicker) {
53
66
  let formatter = DateFormatter()
54
67
  formatter.dateFormat = "dd/MM/yyyy"
@@ -66,6 +79,4 @@ class DatePickerHandler: NSObject {
66
79
  }
67
80
  targetTextField?.resignFirstResponder()
68
81
  }
69
-
70
82
  }
71
-
@@ -39,34 +39,36 @@ public class EnvironmentConfig {
39
39
  self.currentEnvironment = environment
40
40
  }
41
41
 
42
- public enum Endpoints {
43
- case paymentIntent
44
- case hostedCheckouts
45
- case emailVerification
46
- case verifyOtp
47
- case getCards
48
- case creditCharges
49
- case account
50
- case achCharge
51
- case charges
52
- case accountConnect
53
- case threeDSecure
54
-
55
- func path() -> String {
56
- switch self {
57
- case .paymentIntent: return "/api/v1/paymentintent"
58
- case .hostedCheckouts: return "/api/v1/hostedcheckouts"
59
- case .emailVerification: return "/api/v1/customer/send_otp"
60
- case .verifyOtp: return "/api/v1/customer/verify_otp"
61
- // case .getCards: return "/api/v1/customer/card"
62
- case .getCards: return "/api/v1/card"
63
- case .creditCharges: return "/api/v1/customer/charges"
64
- case .account: return "/api/v1/ach/account"
65
- case .achCharge: return "/api/v1/ach/charge"
66
- case .charges: return "/api/v1/charges"
67
- case .accountConnect: return "/api/v1/ach/account/connect"
68
- case .threeDSecure: return "/api/v1/3dsecure"
69
- }
70
- }
71
- }
42
+ public enum Endpoints {
43
+ case paymentIntent
44
+ case hostedCheckouts
45
+ case emailVerification
46
+ case verifyOtp
47
+ case getCards
48
+ case creditCharges
49
+ case account
50
+ case achCharge
51
+ case charges
52
+ case accountConnect
53
+ case threeDSecure
54
+ case threeDSecureStatus(String)
55
+
56
+ func path() -> String {
57
+ switch self {
58
+ case .paymentIntent: return "/api/v1/paymentintent"
59
+ case .hostedCheckouts: return "/api/v1/hostedcheckouts"
60
+ case .emailVerification: return "/api/v1/customer/send_otp"
61
+ case .verifyOtp: return "/api/v1/customer/verify_otp"
62
+ // case .getCards: return "/api/v1/customer/card"
63
+ case .getCards: return "/api/v1/card"
64
+ case .creditCharges: return "/api/v1/customer/charges"
65
+ case .account: return "/api/v1/ach/account"
66
+ case .achCharge: return "/api/v1/ach/charge"
67
+ case .charges: return "/api/v1/charges"
68
+ case .accountConnect: return "/api/v1/ach/account/connect"
69
+ case .threeDSecure: return "/api/v1/3dsecure"
70
+ case .threeDSecureStatus(let token): return "/api/v1/3dsecure/\(token)/status"
71
+ }
72
+ }
73
+ }
72
74
  }