@jimrising/easymerchantsdk-react-native 2.2.9 → 2.3.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 (102) hide show
  1. package/.idea/caches/deviceStreaming.xml +11 -0
  2. package/README.md +310 -171
  3. package/android/build/.transforms/20e1216b87bd06eaab3c9e5c68d4267a/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/reactlibrary/RNEasymerchantsdkModule$2.dex +0 -0
  4. package/android/build/.transforms/20e1216b87bd06eaab3c9e5c68d4267a/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/reactlibrary/RNEasymerchantsdkModule$3.dex +0 -0
  5. package/android/build/.transforms/20e1216b87bd06eaab3c9e5c68d4267a/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/reactlibrary/RNEasymerchantsdkModule.dex +0 -0
  6. package/android/build/.transforms/20e1216b87bd06eaab3c9e5c68d4267a/transformed/bundleLibRuntimeToDirDebug/desugar_graph.bin +0 -0
  7. package/android/build/.transforms/3f3a228346492fb34e3f574e941b632f/transformed/bundleLibRuntimeToDirRelease/bundleLibRuntimeToDirRelease_dex/com/reactlibrary/RNEasymerchantsdkModule$2.dex +0 -0
  8. package/android/build/.transforms/3f3a228346492fb34e3f574e941b632f/transformed/bundleLibRuntimeToDirRelease/bundleLibRuntimeToDirRelease_dex/com/reactlibrary/RNEasymerchantsdkModule$3.dex +0 -0
  9. package/android/build/.transforms/3f3a228346492fb34e3f574e941b632f/transformed/bundleLibRuntimeToDirRelease/bundleLibRuntimeToDirRelease_dex/com/reactlibrary/RNEasymerchantsdkModule.dex +0 -0
  10. package/android/build/.transforms/3f3a228346492fb34e3f574e941b632f/transformed/bundleLibRuntimeToDirRelease/desugar_graph.bin +0 -0
  11. package/android/build/.transforms/58b147bdc43284da2468dab19bc4a64d/transformed/classes/classes_dex/classes.dex +0 -0
  12. package/android/build/.transforms/e9a664a11ce12edf79cd87b1e07aa243/transformed/classes/classes_dex/classes.dex +0 -0
  13. package/android/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar +0 -0
  14. package/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar +0 -0
  15. package/android/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar +0 -0
  16. package/android/build/intermediates/full_jar/release/createFullJarRelease/full.jar +0 -0
  17. package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +1 -1
  18. package/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release-artifact-dependencies.xml +4 -4
  19. package/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release-artifact-libraries.xml +4 -4
  20. package/android/build/intermediates/incremental/release/mergeReleaseResources/compile-file-map.properties +916 -916
  21. package/android/build/intermediates/incremental/release/mergeReleaseResources/merger.xml +3 -3
  22. package/android/build/intermediates/incremental/release/packageReleaseResources/compile-file-map.properties +1 -1
  23. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/reactlibrary/RNEasymerchantsdkModule$2.class +0 -0
  24. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/reactlibrary/RNEasymerchantsdkModule$3.class +0 -0
  25. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/reactlibrary/RNEasymerchantsdkModule.class +0 -0
  26. package/android/build/intermediates/javac/release/compileReleaseJavaWithJavac/classes/com/reactlibrary/RNEasymerchantsdkModule$2.class +0 -0
  27. package/android/build/intermediates/javac/release/compileReleaseJavaWithJavac/classes/com/reactlibrary/RNEasymerchantsdkModule$3.class +0 -0
  28. package/android/build/intermediates/javac/release/compileReleaseJavaWithJavac/classes/com/reactlibrary/RNEasymerchantsdkModule.class +0 -0
  29. package/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/maven.google/com/android/tools/build/group-index.xml +22 -0
  30. package/android/build/intermediates/lint_model/release/generateReleaseLintModel/release-artifact-dependencies.xml +4 -4
  31. package/android/build/intermediates/lint_model/release/generateReleaseLintModel/release-artifact-libraries.xml +4 -4
  32. package/android/build/intermediates/lint_vital_lint_model/release/generateReleaseLintVitalModel/release-artifact-dependencies.xml +4 -4
  33. package/android/build/intermediates/lint_vital_lint_model/release/generateReleaseLintVitalModel/release-artifact-libraries.xml +4 -4
  34. package/android/build/intermediates/local_aar_for_lint/release/out.aar +0 -0
  35. package/android/build/intermediates/merged_res/release/mergeReleaseResources/layout/activity_payment_done_url.xml +2 -1
  36. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/multi-v2/values-night-v8.json +2 -2
  37. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/multi-v2/values.json +38 -38
  38. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/anim-v21.json +9 -9
  39. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/anim.json +24 -24
  40. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/animator-v21.json +1 -1
  41. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/animator.json +34 -34
  42. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/color-night-v8.json +3 -3
  43. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/color-v31.json +10 -10
  44. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/color.json +153 -153
  45. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/drawable-anydpi-v21.json +6 -6
  46. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/drawable-hdpi-v4.json +13 -13
  47. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/drawable-ldpi-v4.json +6 -6
  48. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/drawable-mdpi-v4.json +12 -12
  49. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/drawable-v21.json +4 -4
  50. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/drawable-v23.json +7 -7
  51. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/drawable-v29.json +1 -1
  52. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/drawable-xhdpi-v4.json +12 -12
  53. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/drawable-xxhdpi-v4.json +7 -7
  54. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/drawable-xxxhdpi-v4.json +7 -7
  55. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/drawable.json +395 -395
  56. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/font.json +9 -9
  57. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/interpolator-v21.json +10 -10
  58. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/interpolator.json +11 -11
  59. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/layout-land.json +3 -3
  60. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/layout-sw600dp-v13.json +2 -2
  61. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/layout-v21.json +4 -4
  62. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/layout-v26.json +1 -1
  63. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/layout.json +108 -108
  64. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/mipmap-anydpi-v26.json +2 -2
  65. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/mipmap-hdpi-v4.json +2 -2
  66. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/mipmap-mdpi-v4.json +2 -2
  67. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/mipmap-xhdpi-v4.json +2 -2
  68. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/mipmap-xxhdpi-v4.json +2 -2
  69. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/mipmap-xxxhdpi-v4.json +2 -2
  70. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/raw.json +46 -46
  71. package/android/build/intermediates/merged_res_blame_folder/release/mergeReleaseResources/out/single/xml.json +3 -3
  72. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/reactlibrary/RNEasymerchantsdkModule$2.class +0 -0
  73. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/reactlibrary/RNEasymerchantsdkModule$3.class +0 -0
  74. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/reactlibrary/RNEasymerchantsdkModule.class +0 -0
  75. package/android/build/intermediates/runtime_library_classes_dir/release/bundleLibRuntimeToDirRelease/com/reactlibrary/RNEasymerchantsdkModule$2.class +0 -0
  76. package/android/build/intermediates/runtime_library_classes_dir/release/bundleLibRuntimeToDirRelease/com/reactlibrary/RNEasymerchantsdkModule$3.class +0 -0
  77. package/android/build/intermediates/runtime_library_classes_dir/release/bundleLibRuntimeToDirRelease/com/reactlibrary/RNEasymerchantsdkModule.class +0 -0
  78. package/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar +0 -0
  79. package/android/build/intermediates/runtime_library_classes_jar/release/bundleLibRuntimeToJarRelease/classes.jar +0 -0
  80. package/android/build/intermediates/source_set_path_map/release/mapReleaseSourceSetPaths/file-map.txt +38 -38
  81. package/android/build/intermediates/verified_library_resources/release/verifyReleaseResources/compiled/layout_activity_payment_done_url.xml.flat +0 -0
  82. package/android/build/outputs/aar/jimrising_easymerchantsdk-react-native-release.aar +0 -0
  83. package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
  84. package/android/build/tmp/compileReleaseJavaWithJavac/previous-compilation-data.bin +0 -0
  85. package/android/build.gradle +1 -1
  86. package/android/src/main/java/com/reactlibrary/RNEasymerchantsdkModule.java +56 -1
  87. package/ios/Classes/EasyMerchantSdk.m +124 -141
  88. package/ios/Classes/EasyMerchantSdk.swift +270 -502
  89. package/ios/Classes/EasyPayViewController.swift +18 -15
  90. package/ios/CustomComponents/TextFieldStackView.swift +16 -46
  91. package/ios/EnvironmentConfig.swift +16 -0
  92. package/ios/Example/ViewController.swift +513 -76
  93. package/ios/Models/Request.swift +365 -171
  94. package/ios/Pods/UserDefaults/UserStoreSingleton.swift +116 -12
  95. package/ios/Pods/ViewControllers/AdditionalInfoVC.swift +2 -32
  96. package/ios/Pods/ViewControllers/GrailPayVC.swift +258 -18
  97. package/ios/Pods/ViewControllers/OTPVerificationVC.swift +1 -66
  98. package/ios/Pods/ViewControllers/PaymentInformation/PaymentInfoVC.swift +571 -369
  99. package/ios/Pods/ViewControllers/ThreeDSecurePaymentDoneVC.swift +18 -9
  100. package/ios/easymerchantsdk.podspec +1 -1
  101. package/ios/easymerchantsdk.storyboard +108 -85
  102. package/package.json +1 -1
@@ -4,568 +4,336 @@ import React
4
4
 
5
5
  @objc(EasyMerchantSdkPlugin)
6
6
  public class EasyMerchantSdkPlugin: NSObject, RCTBridgeModule {
7
-
7
+
8
8
  // MARK: - React Native Module Setup
9
+
10
+ @objc
9
11
  public static func moduleName() -> String! {
10
12
  return "EasyMerchantSdk"
11
13
  }
12
-
14
+
15
+ @objc
13
16
  public static func requiresMainQueueSetup() -> Bool {
14
- // We present view controllers and touch UIKit; main queue required.
15
- return true
17
+ return true // UI-related module
16
18
  }
17
-
18
- public var bridge: RCTBridge?
19
-
20
- // MARK: - Stored Promise Callbacks
21
- private var billingResolver: RCTPromiseResolveBlock?
22
- private var billingRejecter: RCTPromiseRejectBlock?
23
- private var paymentReferenceResolver: RCTPromiseResolveBlock?
24
- private var paymentReferenceRejecter: RCTPromiseRejectBlock?
25
-
26
- // MARK: - Notification tokens (so we can remove safely)
27
- private var clientTokenObserver: NSObjectProtocol?
28
- private var referenceObserver: NSObjectProtocol?
29
-
30
- // MARK: - View Controller Reference
19
+
20
+ // MARK: - Properties
21
+
31
22
  private weak var viewController: UIViewController?
32
-
33
- // MARK: - Init
34
- @objc public override init() {
23
+ private var resolve: RCTPromiseResolveBlock?
24
+ private var reject: RCTPromiseRejectBlock?
25
+
26
+ // MARK: - Init / Deinit
27
+
28
+ override public init() {
35
29
  super.init()
30
+ // Removed NotificationCenter observers; using delegate pattern instead
36
31
  }
37
-
38
- public func bridgeDidFinishLoading() {
39
- if bridge != nil {
40
- print("✅ EasyMerchantSdkPlugin bridge initialized")
41
- }
32
+
33
+ deinit {
34
+ // No need for NotificationCenter cleanup
42
35
  }
43
-
44
- // Expose setter for RN to inject the host view controller
45
- @objc public func setViewController(_ vc: UIViewController) {
46
- // Always set on main thread; keep a weak ref to avoid retain cycles.
47
- DispatchQueue.main.async { [weak self] in
48
- self?.viewController = vc
49
- }
50
- }
51
-
52
- @objc public func configureEnvironment(_ env: String, apiKey: String, apiSecret: String) {
36
+
37
+ // MARK: - React Native Exposed Methods
38
+
39
+ @objc(configureEnvironment:apiKey:apiSecret:resolver:rejecter:)
40
+ public func configureEnvironment(_ env: String,
41
+ apiKey: String,
42
+ apiSecret: String,
43
+ resolver resolve: @escaping RCTPromiseResolveBlock,
44
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
45
+ let environment: EnvironmentConfig.Environment
53
46
  switch env.lowercased() {
54
- case "production": EnvironmentConfig.setEnvironment(.production)
55
- case "sandbox": EnvironmentConfig.setEnvironment(.sandbox)
56
- default: EnvironmentConfig.setEnvironment(.staging)
47
+ case "production": environment = .production
48
+ case "sandbox": environment = .sandbox
49
+ case "staging": environment = .staging
50
+ default:
51
+ reject("INVALID_ENVIRONMENT", "Environment must be 'production', 'sandbox', or 'staging'", nil)
52
+ return
57
53
  }
54
+
55
+ EnvironmentConfig.setEnvironment(environment)
58
56
  EnvironmentConfig.configure(apiKey: apiKey, apiSecret: apiSecret)
57
+ resolve("Environment configured successfully")
59
58
  }
60
-
61
- // MARK: - makePayment(...) Exposed to RN - Unified Config Method
62
- @objc public func makePayment(
63
- _ jsonConfig: String,
64
- resolver: @escaping RCTPromiseResolveBlock,
65
- rejecter: @escaping RCTPromiseRejectBlock
66
- ) {
67
- // Parse the JSON config string
68
- guard let configData = jsonConfig.data(using: .utf8),
69
- let config = try? JSONSerialization.jsonObject(with: configData) as? [String: Any] else {
70
- rejecter("INVALID_CONFIG", "Failed to parse JSON config", nil)
59
+
60
+ @objc(makePayment:resolver:rejecter:)
61
+ public func makePayment(_ jsonConfig: String,
62
+ resolver resolve: @escaping RCTPromiseResolveBlock,
63
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
64
+ launchPayment(jsonConfig: jsonConfig, resolve: resolve, reject: reject)
65
+ }
66
+
67
+ @objc(makePaymentV2:resolver:rejecter:)
68
+ public func makePaymentV2(_ jsonConfig: String,
69
+ resolver resolve: @escaping RCTPromiseResolveBlock,
70
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
71
+ launchPayment(jsonConfig: jsonConfig, resolve: resolve, reject: reject)
72
+ }
73
+
74
+ private func launchPayment(jsonConfig: String,
75
+ resolve: @escaping RCTPromiseResolveBlock,
76
+ reject: @escaping RCTPromiseRejectBlock) {
77
+ self.resolve = resolve
78
+ self.reject = reject
79
+
80
+ guard let jsonData = jsonConfig.data(using: .utf8),
81
+ let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else {
82
+ reject("INVALID_JSON", "Failed to parse JSON configuration", nil)
83
+ clearCallbacks()
71
84
  return
72
85
  }
73
86
 
74
- // Extract values from config with defaults
75
- let amount: String
76
- if let amountValue = config["amount"] {
77
- if let amountString = amountValue as? String {
78
- amount = amountString
79
- } else if let amountNumber = amountValue as? NSNumber {
80
- amount = amountNumber.stringValue
81
- } else if let amountDouble = amountValue as? Double {
82
- amount = String(amountDouble)
83
- } else {
84
- amount = "0"
85
- }
86
- } else {
87
- amount = "0"
87
+ guard let presentingVC = viewController ?? getTopViewController() else {
88
+ reject("NO_VIEW_CONTROLLER", "Unable to find a valid view controller", nil)
89
+ clearCallbacks()
90
+ return
88
91
  }
89
92
 
90
- let currency = config["currency"] as? String ?? "usd"
91
- let fields = config["fields"] as? [String: Any]
92
- var paymentMethods = config["paymentMethods"] as? [String] ?? ["card"]
93
-
94
- // If "ach" is present and "bank" is not, convert "ach" to "bank"
95
- if paymentMethods.contains("ach") && !paymentMethods.contains("bank") {
96
- print("🔄 iOS SDK: Converting 'ach' to 'bank' for payment methods")
97
- paymentMethods = paymentMethods.map { method in
98
- method.lowercased() == "ach" ? "bank" : method
99
- }
93
+ let request = createRequest(from: jsonObject)
94
+ if let errorMessage = request.initializationErrorMessage {
95
+ reject("REQUEST_ERROR", errorMessage, nil)
96
+ clearCallbacks()
97
+ return
100
98
  }
101
- let appearanceSettings = config["appearanceSettings"] as? [String: Any]
102
- let tokenOnly = config["tokenOnly"] as? Bool ?? false
103
- let saveCard = config["saveCard"] as? Bool ?? true
104
- let saveAccount = config["saveAccount"] as? Bool ?? true
105
- let authenticatedACH = config["authenticatedACH"] as? Bool ?? false
106
- let grailPayParams = config["grailPayParams"] as? [String: Any]
107
- let submitButtonText = config["submitButtonText"] as? String ?? "Submit"
108
- let isRecurring = config["is_recurring"] as? Bool ?? false
109
- let numOfCycle = config["numOfCycle"] as? Int ?? 1
110
- let recurringIntervals = config["recurringIntervals"] as? [String]
111
- let recurringStartDateType = config["recurringStartDateType"] as? String
112
- let recurringStartDate = config["recurringStartDate"] as? String
113
- let secureAuthentication = config["secureAuthentication"] as? Bool ?? false
114
- let showReceipt = config["showReceipt"] as? Bool ?? true
115
- let showTotal = config["showTotal"] as? Bool ?? true
116
- let showSubmitButton = config["showSubmitButton"] as? Bool ?? true
117
- let isEmail = config["isEmail"] as? Bool ?? true
118
- let email = config["email"] as? String
119
- let name = config["name"] as? String
120
- let enable3DS = false // Default to false for now
121
- let metadata = config["metadata"] as? [String : Any]
122
99
 
123
- // Log what iOS is reading - clean and concise
124
- print("=== iOS SDK: VALUES READ FROM JSON ===")
125
- print("Amount: \(amount)")
126
- print("Currency: \(currency ?? "nil")")
127
- print("Payment Methods: \(paymentMethods)")
128
- print("Email: \(email ?? "nil")")
129
- print("Name: \(name ?? "nil")")
130
- print("Token Only: \(tokenOnly)")
131
- print("Save Card: \(saveCard)")
132
- print("Save Account: \(saveAccount)")
133
- print("Authenticated ACH: \(authenticatedACH)")
134
- print("Is Recurring: \(isRecurring)")
135
- print("Recurring Cycles: \(numOfCycle)")
136
- print("Recurring Intervals: \(recurringIntervals ?? [])")
137
- print("Recurring Start Date: \(recurringStartDate ?? "nil")")
138
- print("Secure Authentication: \(secureAuthentication)")
139
- print("Show Receipt: \(showReceipt)")
140
- print("Show Total: \(showTotal)")
141
- print("Show Submit Button: \(showSubmitButton)")
142
- print("Is Email: \(isEmail)")
143
-
144
- if let appearance = appearanceSettings {
145
- print("Appearance Settings: \(appearance)")
100
+ let controller = EasyPayViewController(request: request, delegate: self)
101
+ DispatchQueue.main.async {
102
+ presentingVC.present(controller, animated: true)
146
103
  }
104
+ }
105
+
106
+ @objc(paymentReference:resolver:rejecter:)
107
+ public func paymentReference(_ referenceToken: String,
108
+ resolver resolve: @escaping RCTPromiseResolveBlock,
109
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
110
+ self.resolve = resolve
111
+ self.reject = reject
147
112
 
148
- if let grail = grailPayParams {
149
- print("Grail Pay Params: \(grail)")
113
+ let request = Request(referenceID: true, referenceToken: referenceToken)
114
+ if let errorMessage = request.initializationErrorMessage {
115
+ reject("REQUEST_ERROR", errorMessage, nil)
116
+ clearCallbacks()
117
+ return
150
118
  }
151
119
 
152
- if let fieldsData = fields {
153
- print("Fields: \(fieldsData)")
120
+ guard let presentingVC = viewController ?? getTopViewController() else {
121
+ reject("NO_VIEW_CONTROLLER", "Unable to find a valid view controller", nil)
122
+ clearCallbacks()
123
+ return
154
124
  }
155
125
 
156
- if let meta = metadata {
157
- print("Metadata: \(meta)")
158
- } else {
159
- print("⚠️ Metadata is nil!")
126
+ let controller = EasyPayViewController(request: request, delegate: self)
127
+ DispatchQueue.main.async {
128
+ presentingVC.present(controller, animated: true)
160
129
  }
161
-
162
- // Call the existing billing method with extracted parameters
163
- billing(
164
- amount,
165
- currency: currency,
166
- fields: fields,
167
- paymentMethods: paymentMethods,
168
- appearanceSettings: appearanceSettings,
169
- tokenOnly: tokenOnly,
170
- saveCard: saveCard,
171
- saveAccount: saveAccount,
172
- authenticatedACH: authenticatedACH,
173
- grailPayParams: grailPayParams,
174
- submitButtonText: submitButtonText,
175
- isRecurring: isRecurring,
176
- numOfCycle: numOfCycle,
177
- recurringIntervals: recurringIntervals,
178
- recurringStartDateType: recurringStartDateType,
179
- recurringStartDate: recurringStartDate,
180
- secureAuthentication: secureAuthentication,
181
- showReceipt: showReceipt,
182
- showTotal: showTotal,
183
- showSubmitButton: showSubmitButton,
184
- isEmail: isEmail,
185
- email: email,
186
- name: name,
187
- enable3DS: enable3DS,
188
- metadata: metadata,
189
- resolver: resolver,
190
- rejecter: rejecter
191
- )
192
130
  }
193
-
194
- // MARK: - billing(...) Exposed to RN
195
- @objc public func billing(
196
- _ amount: String,
197
- currency: String?,
198
- fields: [String: Any]?,
199
- paymentMethods: [String],
200
- appearanceSettings: [String: Any]?,
201
- tokenOnly: Bool,
202
- saveCard: Bool,
203
- saveAccount: Bool,
204
- authenticatedACH: Bool,
205
- grailPayParams: [String: Any]?,
206
- submitButtonText: String?,
207
- isRecurring: Bool,
208
- numOfCycle: Int,
209
- recurringIntervals: [String]?,
210
- recurringStartDateType: String?,
211
- recurringStartDate: String?,
212
- secureAuthentication: Bool,
213
- showReceipt: Bool,
214
- showTotal: Bool,
215
- showSubmitButton: Bool,
216
- isEmail: Bool,
217
- email: String?,
218
- name: String?,
219
- enable3DS: Bool,
220
- metadata: [String: Any]?,
221
- resolver: @escaping RCTPromiseResolveBlock,
222
- rejecter: @escaping RCTPromiseRejectBlock
223
- ) {
224
- // 1) Validate amount
225
- guard let amountValue = Double(amount), amountValue >= 0.50 else {
226
- rejecter("INVALID_AMOUNT", "Amount must be at least $0.50", nil)
227
- return
228
- }
229
-
230
- // 2) Process billing info into FieldSection
231
- var billingInfoData: Data? = nil
232
- if let billingDict = fields {
233
- var billingFields: [FieldItem] = []
234
- var additionalFields: [FieldItem] = []
235
- var billingVisibility = true
236
- var additionalVisibility = true
237
-
238
- // Parse billing fields
239
- if let billing = billingDict["billing"] as? [[String: Any]] {
240
- for field in billing {
241
- guard let name = field["name"] as? String,
242
- let value = field["value"] as? String,
243
- let required = field["required"] as? Bool else { continue }
244
-
245
- if let billingField = BillingFieldName(rawValue: name) {
246
- billingFields.append(FieldItem(name: billingField, required: required, value: value))
247
- }
248
- }
249
- }
250
-
251
- // Parse additional fields
252
- if let additional = billingDict["additional"] as? [[String: Any]] {
253
- for field in additional {
254
- guard let name = field["name"] as? String,
255
- let value = field["value"] as? String,
256
- let required = field["required"] as? Bool else { continue }
257
-
258
- if let additionalField = AdditionalFieldName(rawValue: name) {
259
- additionalFields.append(FieldItem(name: additionalField, required: required, value: value))
260
- }
261
- }
262
- }
263
-
264
- // Parse visibility
265
- if let visibility = billingDict["visibility"] as? [String: Bool] {
266
- billingVisibility = visibility["billing"] ?? true
267
- additionalVisibility = visibility["additional"] ?? true
268
- }
269
-
270
- let field = FieldSection(
271
- visibility: FieldsVisibility(billing: billingVisibility, additional: additionalVisibility),
272
- billing: billingFields,
273
- additional: additionalFields
274
- )
275
-
276
- do {
277
- billingInfoData = try JSONEncoder().encode(field)
278
- } catch {
279
- rejecter("INVALID_BILLING_INFO", "Failed to encode billing info: \(error.localizedDescription)", nil)
280
- return
281
- }
131
+
132
+ @objc(setViewController:resolver:rejecter:)
133
+ public func setViewController(_ viewController: UIViewController,
134
+ resolver resolve: @escaping RCTPromiseResolveBlock,
135
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
136
+ self.viewController = viewController
137
+ resolve("View controller set successfully")
138
+ }
139
+
140
+ @objc(getPlatformVersion:rejecter:)
141
+ public func getPlatformVersion(_ resolve: @escaping RCTPromiseResolveBlock,
142
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
143
+ resolve("iOS \(UIDevice.current.systemVersion)")
144
+ }
145
+
146
+ // MARK: - Helpers
147
+
148
+ private func getTopViewController() -> UIViewController? {
149
+ guard let windowScene = UIApplication.shared.connectedScenes
150
+ .filter({ $0.activationState == .foregroundActive })
151
+ .first as? UIWindowScene,
152
+ let rootVC = windowScene.windows
153
+ .filter({ $0.isKeyWindow })
154
+ .first?.rootViewController else {
155
+ return nil
282
156
  }
283
-
284
- // 3) Map payment methods with ACH to Bank conversion
285
- var processedPaymentMethods = paymentMethods
286
157
 
287
- // If "ach" is present and "bank" is not, convert "ach" to "bank"
288
- if processedPaymentMethods.contains("ach") && !processedPaymentMethods.contains("bank") {
289
- print("🔄 iOS SDK: Converting 'ach' to 'bank' for payment methods")
290
- processedPaymentMethods = processedPaymentMethods.map { method in
291
- method.lowercased() == "ach" ? "bank" : method
158
+ var topVC = rootVC
159
+ while let presentedVC = topVC.presentedViewController {
160
+ topVC = presentedVC
161
+ }
162
+ return topVC
163
+ }
164
+
165
+ private func resolveJSON(_ object: Any) {
166
+ do {
167
+ let jsonData = try JSONSerialization.data(withJSONObject: object, options: [])
168
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
169
+ resolve?(jsonString)
170
+ } else {
171
+ reject?("JSON_ERROR", "Failed to serialize response", nil)
292
172
  }
173
+ } catch {
174
+ reject?("JSON_ERROR", "Serialization failed: \(error.localizedDescription)", error)
293
175
  }
176
+ clearCallbacks()
177
+ }
178
+
179
+ private func clearCallbacks() {
180
+ resolve = nil
181
+ reject = nil
182
+ }
183
+
184
+ private func createRequest(from json: [String: Any]) -> Request {
185
+ let amount = (json["amount"] as? String).flatMap(Double.init)
186
+ let currency = json["currency"] as? String
187
+ let fieldsData = json["fields"].flatMap { try? JSONSerialization.data(withJSONObject: $0, options: []) }
294
188
 
295
- let methods: [PaymentMethod] = processedPaymentMethods.compactMap {
189
+ let paymentMethods = (json["paymentMethods"] as? [String])?.compactMap {
296
190
  switch $0.lowercased() {
297
- case "card": return .Card
298
- case "bank": return .Bank
299
- case "crypto": return .Crypto
300
- case "wallet": return .Wallet
301
- default: return nil
191
+ case "card": return PaymentMethod.Card
192
+ case "ach": return PaymentMethod.Bank
193
+ default: return nil
302
194
  }
303
195
  }
304
196
 
305
- print("📱 iOS SDK: Payment Methods - Original: \(paymentMethods), Processed: \(processedPaymentMethods), Mapped: \(methods)")
306
-
307
- // 4) Prepare GrailPayRequest if needed
308
- var grailParams: GrailPayRequest? = nil
309
- if authenticatedACH, let params = grailPayParams {
310
- grailParams = GrailPayRequest(
311
- role: params["role"] as? String ?? "business",
312
- timeout: params["timeout"] as? Int ?? 10,
313
- isSandbox: params["isSandbox"] as? Bool ?? true,
314
- brandingName: params["brandingName"] as? String ?? "Lyfecycle Payments",
315
- finderSubtitle: params["finderSubtitle"] as? String ?? "Search for your bank",
316
- searchPlaceholder: params["searchPlaceholder"] as? String ?? "Enter bank name"
197
+ let theme = (json["appearanceSettings"] as? [String: Any]).flatMap {
198
+ ThemeConfiguration(
199
+ bodyBackgroundColor: $0["bodyBackgroundColor"] as? String,
200
+ containerBackgroundColor: $0["containerBackgroundColor"] as? String,
201
+ primaryFontColor: $0["primaryFontColor"] as? String,
202
+ secondaryFontColor: $0["secondaryFontColor"] as? String,
203
+ primaryButtonBackgroundColor: $0["primaryButtonBackgroundColor"] as? String,
204
+ primaryButtonHoverColor: $0["primaryButtonHoverColor"] as? String,
205
+ primaryButtonFontColor: $0["primaryButtonFontColor"] as? String,
206
+ secondaryButtonBackgroundColor: $0["secondaryButtonBackgroundColor"] as? String,
207
+ secondaryButtonHoverColor: $0["secondaryButtonHoverColor"] as? String,
208
+ secondaryButtonFontColor: $0["secondaryButtonFontColor"] as? String,
209
+ borderRadius: $0["borderRadius"] as? String,
210
+ fontSize: $0["fontSize"] as? String,
211
+ fontWeight: $0["fontWeight"] as? Int,
212
+ fontFamily: $0["fontFamily"] as? String
317
213
  )
318
214
  }
319
-
320
- // 5) Build theme config
321
- let themeConfig = ThemeConfiguration(
322
- bodyBackgroundColor: appearanceSettings?["bodyBackgroundColor"] as? String ?? "#121212",
323
- containerBackgroundColor: appearanceSettings?["containerBackgroundColor"] as? String ?? "#1E1E1E",
324
- primaryFontColor: appearanceSettings?["primaryFontColor"] as? String ?? "#FFFFFF",
325
- secondaryFontColor: appearanceSettings?["secondaryFontColor"] as? String ?? "#B0B0B0",
326
- primaryButtonBackgroundColor: appearanceSettings?["primaryButtonBackgroundColor"] as? String ?? "#2563EB",
327
- primaryButtonHoverColor: appearanceSettings?["primaryButtonHoverColor"] as? String ?? "#1D4ED8",
328
- primaryButtonFontColor: appearanceSettings?["primaryButtonFontColor"] as? String ?? "#FFFFFF",
329
- secondaryButtonBackgroundColor: appearanceSettings?["secondaryButtonBackgroundColor"] as? String ?? "#374151",
330
- secondaryButtonHoverColor: appearanceSettings?["secondaryButtonHoverColor"] as? String ?? "#4B5563",
331
- secondaryButtonFontColor: appearanceSettings?["secondaryButtonFontColor"] as? String ?? "#E5E7EB",
332
- borderRadius: appearanceSettings?["borderRadius"] as? String ?? "8",
333
- fontSize: appearanceSettings?["fontSize"] as? String ?? "14",
334
- fontWeight: appearanceSettings?["fontWeight"] as? Int ?? 500,
335
- fontFamily: appearanceSettings?["fontFamily"] as? String ?? "\"Inter\", sans-serif"
336
- )
337
-
338
- // 6) Map recurring intervals
339
- let intervals: [RecurringIntervals] = (recurringIntervals ?? []).compactMap { raw in
340
- let cleaned = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
341
- let noDot = cleaned.hasPrefix(".") ? String(cleaned.dropFirst()) : cleaned
342
- return RecurringIntervals(rawValue: noDot)
215
+
216
+ let grailPay = (json["grailPayParams"] as? [String: Any]).flatMap {
217
+ GrailPayRequest(
218
+ role: $0["role"] as? String ?? "",
219
+ timeout: $0["timeout"] as? Int ?? 0,
220
+ brandingName: $0["brandingName"] as? String ?? "",
221
+ finderSubtitle: $0["finderSubtitle"] as? String ?? "",
222
+ searchPlaceholder: $0["searchPlaceholder"] as? String ?? ""
223
+ )
343
224
  }
344
-
345
- if isRecurring && intervals.isEmpty {
346
- rejecter("NO_RECURRING_INTERVALS", "No recurring intervals available.", nil)
347
- return
225
+
226
+ let recurringIntervals = (json["recurringIntervals"] as? [String])?.compactMap {
227
+ switch $0.lowercased() {
228
+ case "daily": return RecurringIntervals.daily
229
+ case "weekly": return .weekly
230
+ case "monthly": return .monthly
231
+ case "quarterly": return .quarterly
232
+ case "yearly": return .yearly
233
+ default: return nil
234
+ }
348
235
  }
349
-
350
- // 7) Map start date type
351
- let startType: RecurringStartDateType? = {
352
- switch recurringStartDateType?.lowercased() {
236
+
237
+ let recurringStartDateType = (json["recurringStartDateType"] as? String).flatMap {
238
+ switch $0.lowercased() {
239
+ case "fixed": return RecurringStartDateType.fixed
353
240
  case "custom": return .custom
354
- case "fixed": return .fixed
355
- default: return nil
356
- }
357
- }()
358
-
359
- // 8) Build the request object
360
- let request = Request(
361
- amount: amountValue,
362
- currency: currency ?? "usd",
363
- fields: billingInfoData,
364
- paymentMethods: methods.isEmpty ? [.Card, .Bank] : methods,
365
- appearanceSettings: themeConfig,
366
- tokenOnly: tokenOnly,
367
- saveCard: saveCard,
368
- saveAccount: saveAccount,
369
- submitButtonText: submitButtonText ?? "Submit",
370
- authenticatedACH: authenticatedACH,
371
- grailPayParams: grailParams,
372
- is_recurring: isRecurring,
373
- numOfCycle: numOfCycle,
374
- recurringIntervals: intervals,
375
- recurringStartDateType: startType,
376
- recurringStartDate: recurringStartDate,
377
- secureAuthentication: secureAuthentication,
378
- showReceipt: showReceipt,
379
- showTotal: showTotal,
380
- showSubmitButton: showSubmitButton,
381
- referenceID: enable3DS,
382
- referenceToken: nil,
383
- isEmail: isEmail,
384
- email: email,
385
- name: name,
386
- metadata: metadata
387
- )
388
-
389
- // Store resolvers
390
- self.billingResolver = resolver
391
- self.billingRejecter = rejecter
392
-
393
- // Handle tokenOnly or present EasyPayViewController
394
- if tokenOnly {
395
- // Register observer directly (no extra Dispatch wrapper).
396
- // Store the token so we can remove it later.
397
- clientTokenObserver = NotificationCenter.default.addObserver(
398
- forName: NSNotification.Name("ClientTokenReceived"),
399
- object: nil,
400
- queue: .main
401
- ) { [weak self] notification in
402
- guard let self = self else { return }
403
- if let clientToken = notification.object as? String {
404
- self.billingResolver?(["status": "success", "clientToken": clientToken])
405
- } else {
406
- self.billingRejecter?("TOKEN_ERROR", "Failed to receive client token.", nil)
407
- }
408
- self.clearResolvers()
409
- }
410
- } else {
411
- // Always present on main thread
412
- DispatchQueue.main.async { [weak self] in
413
- guard let self = self else { return }
414
- let vc = EasyPayViewController(request: request, delegate: self)
415
-
416
- if let host = self.viewController {
417
- host.present(vc, animated: true, completion: nil)
418
- } else if let root = Self.topMostViewController() {
419
- root.present(vc, animated: true, completion: nil)
420
- } else {
421
- rejecter("NO_VIEW_CONTROLLER", "Cannot find a view controller to present.", nil)
422
- self.clearResolvers()
423
- }
241
+ default: return nil
424
242
  }
425
243
  }
426
- }
427
-
428
- // MARK: - paymentReference(...) Exposed to RN
429
- @objc public func paymentReference(
430
- _ referenceToken: String,
431
- resolver: @escaping RCTPromiseResolveBlock,
432
- rejecter: @escaping RCTPromiseRejectBlock
433
- ) {
434
- // Validate reference token
435
- guard !referenceToken.isEmpty else {
436
- rejecter("INVALID_REFERENCE_TOKEN", "Reference token cannot be empty.", nil)
437
- return
244
+
245
+ let environmentString = json["environment"] as? String
246
+ let environment: EnvironmentConfig.Environment
247
+ switch environmentString?.lowercased() {
248
+ case "production": environment = .production
249
+ case "sandbox": environment = .sandbox
250
+ case "staging": environment = .staging
251
+ default: environment = .sandbox
438
252
  }
439
-
440
- // Store resolvers
441
- self.paymentReferenceResolver = resolver
442
- self.paymentReferenceRejecter = rejecter
443
-
444
- // Build the request object (kept for parity; actual flow listens for a notification)
445
- _ = Request(
446
- referenceID: true,
447
- referenceToken: referenceToken
253
+
254
+ return Request(
255
+ amount: amount,
256
+ currency: currency,
257
+ fields: fieldsData,
258
+ paymentMethods: paymentMethods ?? [],
259
+ appearanceSettings: theme,
260
+ tokenOnly: json["tokenOnly"] as? Bool ?? false,
261
+ saveCard: json["saveCard"] as? Bool ?? false,
262
+ saveAccount: json["saveAccount"] as? Bool ?? false,
263
+ submitButtonText: json["submitButtonText"] as? String,
264
+ authenticatedACH: json["authenticatedACH"] as? Bool ?? false,
265
+ grailPayParams: grailPay,
266
+ is_recurring: json["is_recurring"] as? Bool ?? false,
267
+ numOfCycle: json["numOfCycle"] as? Int,
268
+ recurringIntervals: recurringIntervals,
269
+ recurringStartDateType: recurringStartDateType,
270
+ recurringStartDate: json["recurringStartDate"] as? String,
271
+ secureAuthentication: json["secureAuthentication"] as? Bool ?? false,
272
+ showReceipt: json["showReceipt"] as? Bool ?? false,
273
+ showTotal: json["showTotal"] as? Bool ?? false,
274
+ showSubmitButton: json["showSubmitButton"] as? Bool ?? false,
275
+ referenceID: false,
276
+ referenceToken: nil,
277
+ isEmail: json["isEmail"] as? Bool ?? false,
278
+ email: json["email"] as? String,
279
+ name: json["name"] as? String,
280
+ clientToken: json["clientToken"] as? String,
281
+ metadata: json["metadata"] as? [String: Any],
282
+ environment: environment
448
283
  )
449
-
450
- // Register observer directly (no outer Dispatch wrapper).
451
- referenceObserver = NotificationCenter.default.addObserver(
452
- forName: NSNotification.Name("ReferenceIDResponse"),
453
- object: nil,
454
- queue: .main
455
- ) { [weak self] notification in
456
- guard let self = self else { return }
457
- if let data = notification.object as? [String: Any] {
458
- self.paymentReferenceResolver?(data)
459
- } else {
460
- self.paymentReferenceRejecter?("INVALID_RESPONSE", "Invalid response data.", nil)
461
- }
462
- self.clearReferenceResolvers()
463
- }
464
- }
465
-
466
- // MARK: - getPlatformVersion(...) Exposed to RN
467
- @objc public func getPlatformVersion(
468
- _ resolver: RCTPromiseResolveBlock,
469
- rejecter: RCTPromiseRejectBlock
470
- ) {
471
- resolver("iOS \(UIDevice.current.systemVersion)")
472
- }
473
-
474
- // MARK: - Private Helpers
475
- private func clearResolvers() {
476
- billingResolver = nil
477
- billingRejecter = nil
478
- if let token = clientTokenObserver {
479
- NotificationCenter.default.removeObserver(token)
480
- clientTokenObserver = nil
481
- }
482
- }
483
-
484
- private func clearReferenceResolvers() {
485
- paymentReferenceResolver = nil
486
- paymentReferenceRejecter = nil
487
- if let token = referenceObserver {
488
- NotificationCenter.default.removeObserver(token)
489
- referenceObserver = nil
490
- }
491
- // (Old approach of removing by name kept out to avoid double-removal.)
492
- }
493
-
494
- // Find a top-most VC safely (iOS 13+ friendly)
495
- private static func topMostViewController(base: UIViewController? = {
496
- // Use key window from connected scenes
497
- return UIApplication.shared.connectedScenes
498
- .compactMap { $0 as? UIWindowScene }
499
- .flatMap { $0.windows }
500
- .first(where: { $0.isKeyWindow })?
501
- .rootViewController
502
- }()) -> UIViewController? {
503
- if let nav = base as? UINavigationController {
504
- return topMostViewController(base: nav.visibleViewController)
505
- }
506
- if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
507
- return topMostViewController(base: selected)
508
- }
509
- if let presented = base?.presentedViewController {
510
- return topMostViewController(base: presented)
511
- }
512
- return base
513
284
  }
514
285
  }
515
286
 
516
- // MARK: - Delegate Implementation
287
+ // MARK: - EasyPayViewControllerDelegate
288
+
517
289
  extension EasyMerchantSdkPlugin: EasyPayViewControllerDelegate {
518
290
  public func easyPayController(_ controller: EasyPayViewController, didFinishWith result: SDKResult) {
519
- let workItem = DispatchWorkItem { [weak self] in
291
+ DispatchQueue.main.async { [weak self] in
520
292
  guard let self = self else { return }
521
-
293
+
294
+ var response: [String: Any] = [:]
295
+
522
296
  switch result.type {
523
- case .cancelled:
524
- if let message = result.chargeData?["message"] as? String {
525
- self.billingResolver?(["status": "cancelled", "message": message])
526
- } else {
527
- self.billingResolver?(["status": "cancelled"])
528
- }
529
-
530
297
  case .success:
531
- var payload: [String: Any] = ["status": "success"]
532
-
533
- if let cd = result.chargeData {
534
- payload["chargeData"] = cd
535
- }
536
- if let bi = result.billingInfo {
537
- payload["billingInfo"] = bi
298
+ response["status"] = "success"
299
+
300
+ if let chargeData = result.chargeData {
301
+ response["chargeData"] = chargeData
538
302
  }
539
- if let ai = result.additionalInfo {
540
- payload["additionalInfo"] = ai
303
+ if let billing = result.billingInfo {
304
+ response["billingInfo"] = billing
541
305
  }
542
-
543
- // Extract referenceToken for 3DS
544
- if let additional = result.additionalInfo,
545
- let threeDSecureStatus = additional["threeDSecureStatus"] as? [String: Any],
546
- let data = threeDSecureStatus["data"] as? [String: Any],
547
- let token = data["ref_token"] as? String {
548
- payload["referenceToken"] = token
306
+ if let additional = result.additionalInfo {
307
+ response["additionalInfo"] = additional
308
+
309
+ if let threeDSecureStatus = additional["threeDSecureStatus"] as? [String: Any],
310
+ let data = threeDSecureStatus["data"] as? [String: Any],
311
+ let token = data["ref_token"] as? String {
312
+ response["referenceToken"] = token
313
+ }
549
314
  }
550
-
551
- self.billingResolver?(payload)
552
-
315
+
553
316
  case .error:
554
- // Check for an errorMessage in chargeData.
555
- let errorMessage = (result.chargeData?["message"] as? String) ?? "An unknown error occurred."
556
-
557
- let errorResponse: [String: Any] = [
558
- "status": false,
559
- "message": errorMessage
560
- ]
561
-
562
- self.billingResolver?(errorResponse)
317
+ let errorMessage =
318
+ result.error?.localizedDescription ??
319
+ (result.chargeData?["message"] as? String) ??
320
+ "Unknown error occurred"
321
+
322
+ response["status"] = "error"
323
+ response["message"] = errorMessage
324
+
325
+ case .cancelled:
326
+ response["status"] = "cancelled"
327
+ response["message"] = "Payment cancelled"
328
+
329
+ if let chargeData = result.chargeData {
330
+ response["chargeData"] = chargeData
331
+ }
563
332
  }
564
-
565
- self.clearResolvers()
566
- controller.dismiss(animated: true, completion: nil)
333
+
334
+ self.resolveJSON(response)
335
+ controller.dismiss(animated: true)
567
336
  }
568
-
569
- DispatchQueue.main.async(execute: workItem)
570
337
  }
571
338
  }
339
+