@jimrising/easymerchantsdk-react-native 2.4.7 → 2.4.9

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.
@@ -5,40 +5,44 @@
5
5
  // Created by Mony's Mac on 02/05/25.
6
6
  //
7
7
 
8
- import SafariServices
8
+ import UIKit
9
9
 
10
10
  @objc public class GrailPayHelper: NSObject {
11
11
 
12
- private static var safariViewController: SFSafariViewController?
13
12
  private static var completionHandler: ((SDKResult) -> Void)?
14
13
  private static var presentingViewController: UIViewController?
14
+ private static var currentWebViewController: GrailPayWebViewController?
15
15
 
16
16
  public static func presentGrailPay(
17
17
  from viewController: UIViewController,
18
18
  request: GrailPayRequest,
19
+ deepLinkScheme: String? = nil,
19
20
  completion: @escaping (SDKResult) -> Void
20
21
  ) {
21
22
  // Store references
22
23
  completionHandler = completion
23
24
  presentingViewController = viewController
24
25
 
25
- // Launch Safari directly (like Android Chrome Custom Tabs)
26
- launchSafariDirectly(from: viewController, request: request, completion: completion)
26
+ // Get deep link scheme - use provided one or get from bundle identifier
27
+ let scheme = deepLinkScheme ?? Bundle.main.bundleIdentifier ?? "grailpay"
28
+
29
+ // Launch WebView (in-app browser)
30
+ launchWebView(from: viewController, request: request, deepLinkScheme: scheme, completion: completion)
27
31
  }
28
32
 
29
- private static func launchSafariDirectly(
33
+ private static func launchWebView(
30
34
  from viewController: UIViewController,
31
35
  request: GrailPayRequest,
36
+ deepLinkScheme: String,
32
37
  completion: @escaping (SDKResult) -> Void
33
38
  ) {
34
- print("🚀 Loading GrailPay with Safari (direct launch, no intermediate VC)")
35
39
 
36
40
  // Build configuration JSON
37
41
  let configDict: [String: Any] = [
38
42
  "token": UserStoreSingleton.shared.bankWidgetKey ?? "",
39
43
  "vendorId": UserStoreSingleton.shared.vendorID ?? "",
40
44
  "role": request.role,
41
- "timeout": request.timeout * 1000, // Convert to milliseconds
45
+ "timeout": max(request.timeout, 11) * 1000, // Convert to milliseconds; GrailPay minimum is 11 seconds
42
46
  "brandingName": request.brandingName,
43
47
  "finderSubtitle": request.finderSubtitle,
44
48
  "searchPlaceholder": request.searchPlaceholder,
@@ -49,90 +53,187 @@ import SafariServices
49
53
  guard let jsonData = try? JSONSerialization.data(withJSONObject: configDict, options: []),
50
54
  let jsonString = String(data: jsonData, encoding: .utf8),
51
55
  let base64Config = jsonString.data(using: .utf8)?.base64EncodedString() else {
52
- print("❌ Failed to encode configuration")
53
56
  let errorData: NSDictionary = ["status": false, "message": "Failed to encode configuration"]
54
57
  completion(SDKResult(type: .error, data: errorData))
55
58
  return
56
59
  }
57
60
 
58
- print("🔧 Base64 Config:", base64Config)
59
61
 
60
- // Build URL with Base64 config parameter
62
+ // Build URL with Base64 config parameter and deep link scheme
61
63
  let serverHTMLURL = "https://js.lyfepay.io/authenticated-ach.html"
64
+ let timestamp = Int(Date().timeIntervalSince1970 * 1000)
62
65
  var components = URLComponents(string: serverHTMLURL)!
63
66
  components.queryItems = [
64
- URLQueryItem(name: "config", value: base64Config)
67
+ URLQueryItem(name: "config", value: base64Config),
68
+ URLQueryItem(name: "scheme", value: deepLinkScheme),
69
+ URLQueryItem(name: "v", value: "\(timestamp)")
65
70
  ]
66
71
 
67
72
  guard let url = components.url else {
68
- print("❌ Failed to build URL")
69
73
  let errorData: NSDictionary = ["status": false, "message": "Invalid URL configuration"]
70
74
  completion(SDKResult(type: .error, data: errorData))
71
75
  return
72
76
  }
73
77
 
74
- print("🌐 Safari URL:", url.absoluteString)
75
78
 
76
- // Configure Safari View Controller
77
- let safari = SFSafariViewController(url: url)
78
- safari.delegate = SafariDelegateHandler.shared
79
- SafariDelegateHandler.shared.completion = completion
79
+ // Create and present WebView controller
80
+ let webViewController = GrailPayWebViewController(url: url, completion: completion)
81
+ webViewController.modalPresentationStyle = .fullScreen
82
+
83
+ // Store reference to current WebView controller
84
+ currentWebViewController = webViewController
80
85
 
81
- if #available(iOS 11.0, *) {
82
- safari.dismissButtonStyle = .close
83
- safari.preferredBarTintColor = .white
84
- safari.preferredControlTintColor = .systemBlue
86
+ viewController.present(webViewController, animated: true) {
85
87
  }
88
+ }
89
+
90
+ /// Find the currently open GrailPayWebViewController
91
+ private static func findOpenWebViewController() -> GrailPayWebViewController? {
92
+ // First check stored reference
93
+ if let webVC = currentWebViewController, webVC.isViewLoaded && webVC.view.window != nil {
94
+ return webVC
95
+ }
96
+
97
+ // Search through presented view controllers
98
+ guard let topVC = getTopViewController() else { return nil }
86
99
 
87
- // Present Safari directly from the current view controller
88
- safariViewController = safari
89
- viewController.present(safari, animated: true) {
90
- print("✅ Safari presented directly (no white screen)")
100
+ // Check if top VC is GrailPayWebViewController
101
+ if let webVC = topVC as? GrailPayWebViewController {
102
+ return webVC
103
+ }
104
+
105
+ // Check presented view controller
106
+ if let presented = topVC.presentedViewController as? GrailPayWebViewController {
107
+ return presented
108
+ }
109
+
110
+ // Check navigation stack
111
+ if let navVC = topVC as? UINavigationController,
112
+ let webVC = navVC.topViewController as? GrailPayWebViewController {
113
+ return webVC
114
+ }
115
+
116
+ // Search through parent hierarchy
117
+ var currentVC: UIViewController? = topVC
118
+ while let vc = currentVC {
119
+ if let webVC = vc as? GrailPayWebViewController {
120
+ return webVC
121
+ }
122
+ currentVC = vc.presentedViewController ?? vc.parent
91
123
  }
124
+
125
+ return nil
126
+ }
127
+
128
+ /// Handle GrailPay OAuth callback intercepted from bank app redirect.
129
+ /// Note: With WebView, OAuth callbacks are handled within the WebView itself.
130
+ @objc(handleGrailPayOAuthCallbackWithUrl:) public static func handleGrailPayOAuthCallback(url: URL) {
131
+ // With WebView implementation, OAuth callbacks are automatically handled
132
+ // within the WebView's navigation delegate
133
+ }
134
+
135
+ /// Get the top-most view controller
136
+ private static func getTopViewController() -> UIViewController? {
137
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
138
+ let rootVC = windowScene.windows.first?.rootViewController else {
139
+ return nil
140
+ }
141
+
142
+ var topVC = rootVC
143
+ while let presentedVC = topVC.presentedViewController {
144
+ topVC = presentedVC
145
+ }
146
+ return topVC
147
+ }
148
+
149
+ /// Clear the current WebView controller reference
150
+ static func clearWebViewController() {
151
+ currentWebViewController = nil
92
152
  }
93
153
 
94
154
  // Handle deep link callback
95
- @objc public static func handleDeepLinkCallback(url: URL) {
96
- guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
97
- let dataParam = components.queryItems?.first(where: { $0.name == "data" })?.value else {
98
- print("❌ Invalid deep link format")
155
+ @objc(handleDeepLinkCallbackWithUrl:) public static func handleDeepLinkCallback(url: URL) {
156
+ let urlString = url.absoluteString
157
+ let scheme = url.scheme?.lowercased() ?? ""
158
+
159
+
160
+ // Handle expediter/expeditor schemes - these contain OAuth data directly in query params
161
+ if scheme == "expediter" || scheme == "expeditor" {
162
+
163
+ // Extract all query parameters as OAuth data
164
+ guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
165
+ return
166
+ }
167
+
168
+ var oauthData: [String: String] = [:]
169
+ components.queryItems?.forEach { item in
170
+ if let value = item.value {
171
+ oauthData[item.name] = value
172
+ }
173
+ }
174
+ oauthData["url"] = urlString
175
+
176
+ // Try to find open WebView and inject OAuth data
177
+ if let openWebViewController = findOpenWebViewController() {
178
+ DispatchQueue.main.async {
179
+ openWebViewController.forwardOAuthDataToMainWebView(oauthData)
180
+ }
181
+ return
182
+ }
183
+
184
+ // If no WebView is open, try to use completion handler
185
+ if let storedCompletionHandler = completionHandler {
186
+ // For expediter/expeditor, we might not have eventType/data structure
187
+ // So we'll wrap it in a success result
188
+ let resultData: NSDictionary = ["data": [oauthData]]
189
+ let result = SDKResult(type: .success, data: resultData)
190
+ completionHandler = nil
191
+ storedCompletionHandler(result)
192
+ } else {
193
+ }
194
+ return
195
+ }
196
+
197
+ // Handle grailpay:// scheme with base64 encoded data
198
+ guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
199
+ return
200
+ }
201
+
202
+ guard let dataParam = components.queryItems?.first(where: { $0.name == "data" })?.value else {
203
+ if let queryItems = components.queryItems {
204
+ let itemNames = queryItems.map { $0.name }
205
+ } else {
206
+ }
99
207
  return
100
208
  }
101
209
 
102
- print("🔗 Deep link received:", url.absoluteString)
103
- print("📦 Data param:", dataParam)
104
210
 
105
211
  // URL decode
106
212
  guard let urlDecoded = dataParam.removingPercentEncoding else {
107
- print("❌ Failed to URL decode")
108
213
  return
109
214
  }
110
215
 
111
216
  // Base64 decode
112
217
  guard let decodedData = Data(base64Encoded: urlDecoded),
113
218
  let decodedString = String(data: decodedData, encoding: .utf8) else {
114
- print("❌ Failed to Base64 decode")
115
219
  return
116
220
  }
117
221
 
118
- print("📋 Decoded JSON:", decodedString)
119
222
 
120
223
  // Parse JSON
121
224
  guard let jsonData = decodedString.data(using: .utf8),
122
225
  let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
123
226
  let eventType = json["eventType"] as? String,
124
227
  let data = json["data"] else {
125
- print("❌ Failed to parse JSON")
126
228
  return
127
229
  }
128
230
 
129
- print("✅ Event type:", eventType)
130
- print("📊 Data:", data)
131
231
 
132
- // Dismiss Safari
133
- safariViewController?.dismiss(animated: true) {
134
- print("🚪 Safari dismissed")
135
- }
232
+ // Store reference before clearing
233
+ let storedCompletionHandler = completionHandler
234
+
235
+ // Clear static reference immediately
236
+ completionHandler = nil
136
237
 
137
238
  // Handle callback based on event type
138
239
  switch eventType {
@@ -140,7 +241,7 @@ import SafariServices
140
241
  // Wrap account data in an array to match expected format
141
242
  let resultData: NSDictionary = ["data": [data]]
142
243
  let result = SDKResult(type: .success, data: resultData)
143
- completionHandler?(result)
244
+ storedCompletionHandler?(result)
144
245
 
145
246
  case "linkExit":
146
247
  if let exitData = data as? [String: Any],
@@ -149,32 +250,19 @@ import SafariServices
149
250
  // Wrap account data in an array to match expected format
150
251
  let resultData: NSDictionary = ["data": [data]]
151
252
  let result = SDKResult(type: .success, data: resultData)
152
- completionHandler?(result)
253
+ storedCompletionHandler?(result)
153
254
  } else {
154
255
  let resultData: NSDictionary = ["data": data]
155
256
  let result = SDKResult(type: .cancelled, data: resultData)
156
- completionHandler?(result)
257
+ storedCompletionHandler?(result)
157
258
  }
158
259
 
159
260
  case "error":
160
261
  let errorData: NSDictionary = ["status": false, "message": "GrailPay error: \(data)"]
161
262
  let result = SDKResult(type: .error, data: errorData)
162
- completionHandler?(result)
263
+ storedCompletionHandler?(result)
163
264
 
164
265
  default:
165
- print("⚠️ Unknown event type:", eventType)
166
266
  }
167
267
  }
168
268
  }
169
-
170
- // MARK: - Safari Delegate Handler
171
- private class SafariDelegateHandler: NSObject, SFSafariViewControllerDelegate {
172
- static let shared = SafariDelegateHandler()
173
- var completion: ((SDKResult) -> Void)?
174
-
175
- func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
176
- print("🚪 Safari dismissed by user (cancel)")
177
- // Only call completion if no deep link was received
178
- // (deep link handler will call completion with success)
179
- }
180
- }