react-native-security-suite 0.7.2 → 0.8.0

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.
package/README.md CHANGED
@@ -3,13 +3,7 @@
3
3
  Security solutions for React Native both platform Android and iOS
4
4
  You can use any of the following:
5
5
 
6
- <ol>
7
- <li>Android Root device or iOS Jailbreak device detection</li>
8
- <li>Text Encryption/Decryption</li>
9
- <li>Secure storage</li>
10
- <li>Diffie–Hellman key exchange</li>
11
- <li>SSL Pinning & public key pinning</li>
12
- </ol>
6
+ <ol><li>Android Root device or iOS Jailbreak device detection</li><li>Text Encryption/Decryption</li><li>Secure storage</li><li>Diffie–Hellman key exchange</li><li>SSL Pinning &amp; public key pinning</li><li>Network Logger (Android Chucker - iOS Pulse)</li></ol>
13
7
 
14
8
  ## Installation
15
9
 
@@ -32,8 +26,7 @@ const isRiskyDevice = await deviceHasSecurityRisk();
32
26
  console.log('Root/Jailbreak detection result: ', isRiskyDevice);
33
27
  ```
34
28
 
35
- \
36
- 2. Text Encryption/Decryption example:
29
+ 2\. Text Encryption/Decryption example:
37
30
 
38
31
  ```js
39
32
  const softEncrypted = await encrypt('STR_FOR_ENCRYPT');
@@ -42,8 +35,7 @@ const softDecrypted = await decrypt('STR_FOR_DECRYPT');
42
35
  console.log('Decrypted result: ', softDecrypted);
43
36
  ```
44
37
 
45
- \
46
- 3. Secure storage example:
38
+ 3\. Secure storage example:
47
39
 
48
40
  ```js
49
41
  import { SecureStorage } from 'react-native-security-suite';
@@ -52,8 +44,7 @@ SecureStorage.setItem('key', 'value');
52
44
  console.log(await SecureStorage.getItem('key'));
53
45
  ```
54
46
 
55
- \
56
- 4. Diffie–Hellman key exchange:
47
+ 4\. Diffie–Hellman key exchange:
57
48
 
58
49
  ```js
59
50
  import {
@@ -80,8 +71,7 @@ const hardDecrypted = await decryptBySharedKey('STR_FOR_DECRYPT');
80
71
  console.log('Decrypted result: ', hardDecrypted);
81
72
  ```
82
73
 
83
- \
84
- 5. SSL Pinning with android network logger example:
74
+ 5\. SSL Pinning with network logger:
85
75
 
86
76
  ```js
87
77
  import { fetch } from 'react-native-security-suite';
@@ -0,0 +1,82 @@
1
+ import UserNotifications
2
+ import PulseUI
3
+ import SwiftUI
4
+
5
+ @available(iOS 13.0, *)
6
+ class PulseUINotification: NSObject, UNUserNotificationCenterDelegate {
7
+ override init() {
8
+ super.init()
9
+
10
+ UNUserNotificationCenter.current().delegate = self
11
+ }
12
+
13
+ func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
14
+ completionHandler([.sound, .badge])
15
+ }
16
+
17
+ func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
18
+ switch response.actionIdentifier {
19
+ case "openPulse":
20
+ openPulseUI()
21
+ break
22
+ default:
23
+ openPulseUI()
24
+ break
25
+ }
26
+ completionHandler()
27
+ }
28
+
29
+ func requestPermission() {
30
+ UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
31
+ if granted {
32
+ // print("PulseUI Notification Permission Granted")
33
+ } else {
34
+ print("PulseUI Notification Permission Denied")
35
+ }
36
+ }
37
+ }
38
+
39
+ func showNotification(body: String) {
40
+ requestPermission()
41
+
42
+ let content = UNMutableNotificationContent()
43
+ content.title = "Recording HTTP Activity"
44
+ content.body = body
45
+ content.sound = .none
46
+ content.badge = 1
47
+
48
+ let action = UNNotificationAction(identifier: "openPulse", title: "Open Pulse", options: [])
49
+ let category = UNNotificationCategory(identifier: "pulseCategory", actions: [action], intentIdentifiers: [], options: [])
50
+
51
+ UNUserNotificationCenter.current().setNotificationCategories([category])
52
+ content.categoryIdentifier = "pulseCategory"
53
+
54
+ let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
55
+
56
+ let request = UNNotificationRequest(identifier: "pulseNotification", content: content, trigger: trigger)
57
+
58
+ UNUserNotificationCenter.current().add(request) { error in
59
+ if let error = error {
60
+ print("Error scheduling notification: \(error.localizedDescription)")
61
+ }
62
+ }
63
+ }
64
+
65
+ @objc(openPulseUI)
66
+ func openPulseUI() {
67
+ DispatchQueue.main.async {
68
+ if #available(iOS 15.0, *) {
69
+ let hostingController = UIHostingController(rootView: ConsoleView())
70
+ if let rootViewController = UIApplication.shared.keyWindow?.rootViewController {
71
+ let navigationController = UINavigationController()
72
+ navigationController.setViewControllers([hostingController], animated: false)
73
+ rootViewController.present(navigationController, animated: true, completion: nil)
74
+ }
75
+ } else {
76
+ if let rootViewController = UIApplication.shared.keyWindow?.rootViewController {
77
+ rootViewController.present(UIViewController(), animated: true, completion: nil)
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
@@ -11,8 +11,9 @@ class SecuritySuite: NSObject {
11
11
  var privateKey: String!,
12
12
  publicKey: String!,
13
13
  sharedKey: String!,
14
- keyData: Data!
15
-
14
+ keyData: Data!,
15
+ session: URLSession!
16
+
16
17
  @objc(getPublicKey:withRejecter:)
17
18
  func getPublicKey(resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
18
19
  do {
@@ -127,62 +128,43 @@ class SecuritySuite: NSObject {
127
128
 
128
129
  @objc(fetch:withData:withCallback:)
129
130
  func fetch(url: NSString, data: NSDictionary, callback: @escaping RCTResponseSenderBlock) -> Void {
130
- let config = URLSessionConfiguration.default
131
- config.httpShouldSetCookies = false
132
- config.httpCookieAcceptPolicy = .never
133
- config.networkServiceType = .responsiveData
134
- config.shouldUseExtendedBackgroundIdleMode = true
135
-
136
- let sslPinning = SSLPinning(data: data)
131
+ let configuration = URLSessionConfiguration.default
132
+ configuration.httpShouldSetCookies = false
133
+ configuration.httpCookieAcceptPolicy = .never
134
+ configuration.networkServiceType = .responsiveData
135
+ configuration.shouldUseExtendedBackgroundIdleMode = true
137
136
 
138
- let startTime = Date()
139
137
  var request = URLRequest(url: URL(string: url as String)!)
140
-
141
138
  request.httpMethod = data["method"] as? String ?? "POST"
142
139
  request.timeoutInterval = data["timeout"] as? TimeInterval ?? 60.0
143
140
  request.allHTTPHeaderFields = data["headers"] as? [String : String]
144
- if data["body"] != nil { request.httpBody = (data["body"] as! String).data(using: .utf8)! } else { request.httpBody = "".data(using: .utf8)! }
145
-
146
- if data["keyId"] != nil && data["requestId"] != nil {
147
- request.setValue(jwsHeader(payload: request.httpBody ?? .init(), keyId: data["keyId"] as! String, requestId: data["requestId"] as! String), forHTTPHeaderField: "X-JWS-Signature")
148
- }
149
-
150
- let session = URLSession(configuration: config, delegate: sslPinning, delegateQueue: .main)
151
-
152
- let task = session.dataTask(with: request) { data, response, error in
153
- let response = response as? HTTPURLResponse
154
-
155
- if error == nil {
156
- let responseCode = response?.statusCode
157
- let responseString = String.init(decoding: data ?? .init(), as: UTF8.self)
158
- let errorString = error?.localizedDescription
159
- let responseJSON = try? JSONSerialization.jsonObject(with: data!, options: [])
160
-
161
- var result:NSMutableDictionary = [
162
- "status": response?.statusCode,
163
- "url": url,
164
- ]
165
- if errorString == nil && responseCode! < 400 {
166
- result["response"] = responseString
167
- result["duration"] = "\(Int(Date().timeIntervalSince(startTime) * 1000))ms"
168
- result["responseJSON"] = responseJSON
169
- callback([result, NSNull()])
170
- } else {
171
- result["error"] = responseString
172
- result["errorJSON"] = responseJSON
173
- do {
174
- let jsonData = try JSONSerialization.data(withJSONObject: result)
175
- callback([NSNull(), result])
176
- } catch {
177
- callback([NSNull(), "JSON_PARSE_ERROR"])
178
- }
179
- }
180
- } else {
181
- callback([NSNull(), "MUST_BE_UPDATE"])
141
+ // Prepare body
142
+ if let body = data["body"] {
143
+ if let bodyString = body as? String {
144
+ request.httpBody = bodyString.data(using: .utf8)
145
+ } else if let bodyDict = body as? [String: Any] {
146
+ request.httpBody = try? JSONSerialization.data(withJSONObject: bodyDict, options: [])
182
147
  }
183
148
  }
184
-
185
- task.resume()
149
+ if data["keyId"] != nil && data["requestId"] != nil {
150
+ request
151
+ .setValue(
152
+ jwsHeader(
153
+ payload: request.httpBody ?? .init(),
154
+ keyId: data["keyId"] as! String,
155
+ requestId: data["requestId"] as! String
156
+ ),
157
+ forHTTPHeaderField: "X-JWS-Signature"
158
+ )
159
+ }
160
+
161
+ let sslPinning = SSLPinning(url: url, data: data, callback: callback)
162
+ let session = URLSession(
163
+ configuration: configuration,
164
+ delegate: sslPinning,
165
+ delegateQueue: .main
166
+ )
167
+ session.dataTask(with: request).resume()
186
168
  }
187
169
 
188
170
  @objc(deviceHasSecurityRisk:withRejecter:)
@@ -223,32 +205,3 @@ class SecuritySuite: NSObject {
223
205
  }
224
206
  }
225
207
  }
226
-
227
- struct ASN1 {
228
- static let rsa2048 = Data(base64Encoded: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A")!
229
- static let rsa4096 = Data(base64Encoded: "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A")!
230
- static let ec256 = Data(base64Encoded: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgA=")!
231
- static let ec384 = Data(base64Encoded: "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgA=")!
232
- static let ec521 = Data(base64Encoded: "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQ=")!
233
- }
234
-
235
- struct JoseHeader: Codable {
236
- internal init(alg: String = "HS256", kid: String, b64: Bool = false, crit: [String] = ["b64"], requestId: String) {
237
- self.alg = alg
238
- self.kid = kid
239
- self.b64 = b64
240
- self.crit = crit
241
- self.requestId = requestId
242
- }
243
-
244
- private enum CodingKeys: String, CodingKey {
245
- case alg, kid, b64, crit
246
- case requestId = "request_id"
247
- }
248
-
249
- let alg: String
250
- let kid: String
251
- let b64: Bool
252
- let crit: [String]
253
- let requestId: String
254
- }
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Workspace
3
+ version = "1.0">
4
+ <FileRef
5
+ location = "self:">
6
+ </FileRef>
7
+ </Workspace>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>IDEDidComputeMac32BitWarning</key>
6
+ <true/>
7
+ </dict>
8
+ </plist>
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>SchemeUserState</key>
6
+ <dict>
7
+ <key>SecuritySuite.xcscheme_^#shared#^_</key>
8
+ <dict>
9
+ <key>orderHint</key>
10
+ <integer>0</integer>
11
+ </dict>
12
+ </dict>
13
+ </dict>
14
+ </plist>
@@ -1,71 +1,155 @@
1
- import Foundation
1
+ import Pulse
2
2
 
3
3
  @available(iOS 13.0, *)
4
- class SSLPinning: NSObject, URLSessionDelegate {
5
- var validDomains: [String] = []
6
- var intermediateKeyHashes: [Data] = []
7
- var leafKeyHashes: [Data] = []
8
-
9
- init(data: NSDictionary) {
10
- if let certs = data["certificates"] as? [String] {
11
- for cert in certs {
12
- var filteredCert = cert.replacingOccurrences(of: "sha256/", with: "", options: .caseInsensitive, range: nil)
13
- intermediateKeyHashes.append(Data(base64Encoded: filteredCert)!)
14
- }
15
- }
4
+ class SSLPinning: NSObject, URLSessionDataDelegate {
5
+ var url: NSString!
6
+ var data: NSDictionary!
7
+ var callback: RCTResponseSenderBlock!
8
+ var validDomains: [String] = []
9
+ var intermediateKeyHashes: [Data] = []
10
+ var leafKeyHashes: [Data] = []
11
+ var networkLogger: NetworkLogger = .init()
12
+ var responseData: Data = Data()
13
+ let pulseNotification = PulseUINotification()
14
+
15
+ init(url: NSString, data: NSDictionary, callback: @escaping RCTResponseSenderBlock) {
16
+ self.url = url
17
+ self.data = data
18
+ self.callback = callback
19
+ }
20
+
21
+ func urlSession(
22
+ _ session: URLSession,
23
+ didReceive challenge: URLAuthenticationChallenge,
24
+ completionHandler: @escaping (
25
+ URLSession.AuthChallengeDisposition,
26
+ URLCredential?
27
+ ) -> Void
28
+ ) {
29
+ if let certs = data["certificates"] as? [String] {
30
+ for cert in certs {
31
+ let filteredCert = cert.replacingOccurrences(
32
+ of: "sha256/",
33
+ with: "",
34
+ options: .caseInsensitive,
35
+ range: nil
36
+ )
37
+ intermediateKeyHashes.append(Data(base64Encoded: filteredCert)!)
38
+ }
39
+ } else {
40
+ return completionHandler(.performDefaultHandling, nil)
41
+ }
16
42
 
17
- if let domains = data["validDomains"] as? [String] {
18
- for domain in domains {
19
- validDomains.append(domain)
20
- }
21
- }
43
+ if let domains = data["validDomains"] as? [String] {
44
+ for domain in domains {
45
+ validDomains.append(domain)
46
+ }
47
+ } else {
48
+ return completionHandler(.performDefaultHandling, nil)
22
49
  }
23
-
24
- func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
25
- guard let trust = challenge.protectionSpace.serverTrust else {
26
- return completionHandler(.cancelAuthenticationChallenge, nil)
27
- }
50
+
51
+ guard let trust = challenge.protectionSpace.serverTrust else {
52
+ return completionHandler(.cancelAuthenticationChallenge, nil)
53
+ }
54
+
55
+ let host = challenge.protectionSpace.host
56
+ let port = challenge.protectionSpace.port
57
+ guard port == 443, (3...4).contains(trust.certificates.count),
58
+ let leafCertificate = trust.certificates.first,
59
+ let commonName = leafCertificate.commonName,
60
+ validDomains
61
+ .contains(where: { commonName == $0 || commonName.hasSuffix("." + $0) }) else {
62
+ completionHandler(.cancelAuthenticationChallenge, nil)
63
+ return
64
+ }
65
+ let intermediateCertificatesValid = trust.certificates.dropFirst().prefix(2).allSatisfy {
66
+ ($0.pin.map(intermediateKeyHashes.contains) ?? false)
67
+ }
68
+ let leafCertificateValid = leafKeyHashes.contains(
69
+ leafCertificate.pin ?? .init()
70
+ )
28
71
 
29
- let host = challenge.protectionSpace.host
30
- let port = challenge.protectionSpace.port
31
- guard port == 443, (3...4).contains(trust.certificates.count),
32
- let leafCertificate = trust.certificates.first,
33
- let commonName = leafCertificate.commonName,
34
- validDomains.contains(where: { commonName == $0 || commonName.hasSuffix("." + $0) }) else {
35
- completionHandler(.cancelAuthenticationChallenge, nil)
36
- return
37
- }
38
- let intermediateCertificatesValid = trust.certificates.dropFirst().prefix(2).allSatisfy {
39
- ($0.pin.map(intermediateKeyHashes.contains) ?? false)
40
- }
41
- let leafCertificateValid = leafKeyHashes.contains(leafCertificate.pin ?? .init())
72
+ let pattern = commonName
73
+ .replacingOccurrences(of: ".", with: "\\.")
74
+ .replacingOccurrences(of: "*", with: ".+", options: [.anchored])
75
+ guard intermediateCertificatesValid && (
76
+ leafKeyHashes.isEmpty || leafCertificateValid
77
+ ),
78
+ let commonNameRegex = try? NSRegularExpression(pattern: pattern) else {
79
+ completionHandler(.cancelAuthenticationChallenge, nil)
80
+ return
81
+ }
42
82
 
43
- let pattern = commonName
44
- .replacingOccurrences(of: ".", with: "\\.")
45
- .replacingOccurrences(of: "*", with: ".+", options: [.anchored])
46
- guard intermediateCertificatesValid && (leafKeyHashes.isEmpty || leafCertificateValid),
47
- let commonNameRegex = try? NSRegularExpression(pattern: pattern) else {
48
- completionHandler(.cancelAuthenticationChallenge, nil)
49
- return
50
- }
83
+ guard commonNameRegex.textMatches(in: host) == [host] else {
84
+ completionHandler(.cancelAuthenticationChallenge, nil)
85
+ return
86
+ }
51
87
 
52
- guard commonNameRegex.textMatches(in: host) == [host] else {
53
- completionHandler(.cancelAuthenticationChallenge, nil)
54
- return
55
- }
88
+ trust.policies = [.ssl(server: true, hostname: host)]
89
+ do {
90
+ if try !trust.evaluate() {
91
+ completionHandler(.cancelAuthenticationChallenge, nil)
92
+ }
93
+ } catch {
94
+ completionHandler(.cancelAuthenticationChallenge, nil)
95
+ return
96
+ }
56
97
 
57
- trust.policies = [.ssl(server: true, hostname: host)]
58
- do {
59
- if try !trust.evaluate() {
60
- completionHandler(.cancelAuthenticationChallenge, nil)
98
+ let credential = URLCredential(trust: trust)
99
+ completionHandler(.useCredential, credential)
100
+ }
101
+
102
+ func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) {
103
+ networkLogger.logTaskCreated(task)
104
+ }
105
+
106
+ func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
107
+ networkLogger.logDataTask(dataTask, didReceive: data)
108
+ responseData.append(data)
109
+ }
110
+
111
+ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
112
+ networkLogger.logTask(task, didCompleteWithError: error)
113
+
114
+ if let error = error {
115
+ callback([nil, error.localizedDescription])
116
+ } else {
117
+ if let httpResponse = task.response as? HTTPURLResponse {
118
+ let url = httpResponse.url?.absoluteString ?? url! as String
119
+ let status = httpResponse.statusCode
120
+ let headers = httpResponse.allHeaderFields
121
+
122
+ if(httpResponse.statusCode > 299) {
123
+ callback([NSNull(), [
124
+ "url": url,
125
+ "status": status,
126
+ "headers": status,
127
+ "error": String(data: responseData, encoding: .utf8) ?? "",
128
+ ]])
129
+ } else {
130
+ callback([[
131
+ "url": url,
132
+ "status": status,
133
+ "headers": headers,
134
+ "response": String.init(decoding: responseData, as: UTF8.self),
135
+ "responseJson": try? JSONSerialization.jsonObject(with: responseData, options: []),
136
+ ], NSNull()])
137
+ }
138
+ } else {
139
+ callback([NSNull(), "Unknown error occurred"])
140
+ }
141
+ }
142
+ }
143
+
144
+ func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
145
+ networkLogger.logTask(task, didFinishCollecting: metrics)
146
+ if (data["loggerIsEnabled"] as? Bool) == true {
147
+ if let httpResponse = task.response as? HTTPURLResponse {
148
+ if let url = URL(string: httpResponse.url?.absoluteString ?? url! as String) {
149
+ pulseNotification.showNotification(body: "\(httpResponse.statusCode) \(url.relativePath)")
61
150
  }
62
- } catch {
63
- completionHandler(.cancelAuthenticationChallenge, nil)
64
- return
151
+ }
65
152
  }
66
-
67
- let credential = URLCredential(trust: trust)
68
- completionHandler(.useCredential, credential)
69
153
  }
70
154
  }
71
155
 
@@ -196,3 +280,24 @@ extension NSRegularExpression {
196
280
  }
197
281
  }
198
282
  }
283
+
284
+ struct JoseHeader: Codable {
285
+ internal init(alg: String = "HS256", kid: String, b64: Bool = false, crit: [String] = ["b64"], requestId: String) {
286
+ self.alg = alg
287
+ self.kid = kid
288
+ self.b64 = b64
289
+ self.crit = crit
290
+ self.requestId = requestId
291
+ }
292
+
293
+ private enum CodingKeys: String, CodingKey {
294
+ case alg, kid, b64, crit
295
+ case requestId = "request_id"
296
+ }
297
+
298
+ let alg: String
299
+ let kid: String
300
+ let b64: Bool
301
+ let crit: [String]
302
+ let requestId: String
303
+ }
@@ -79,3 +79,11 @@ public extension Data {
79
79
  .joined()
80
80
  }
81
81
  }
82
+
83
+ struct ASN1 {
84
+ static let rsa2048 = Data(base64Encoded: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A")!
85
+ static let rsa4096 = Data(base64Encoded: "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A")!
86
+ static let ec256 = Data(base64Encoded: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgA=")!
87
+ static let ec384 = Data(base64Encoded: "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgA=")!
88
+ static let ec521 = Data(base64Encoded: "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQ=")!
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-security-suite",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Security solution for React Native",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/commonjs/index.js",
@@ -18,6 +18,8 @@ Pod::Spec.new do |s|
18
18
 
19
19
  s.dependency "React-Core"
20
20
  s.dependency "IOSSecuritySuite"
21
+ s.dependency "PulseCore"
22
+ s.dependency "PulseUI"
21
23
 
22
24
  # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
23
25
  # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.