@rn-tools/sheets 0.1.4 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,83 +1,133 @@
1
1
  import ExpoModulesCore
2
2
  import React
3
- import SwiftUI
3
+ import UIKit
4
4
 
5
- public class SheetProps: ObservableObject {
6
- @Published var children: [UIView] = []
7
- @Published var isOpen: Bool = false
8
- @Published var openToIndex: Int = 0
9
- @Published var snapPoints: [Int] = []
5
+ public class SheetProps {
6
+ var isOpen: Bool = false
7
+ var initialIndex: Int = 0
8
+ var snapPoints: [CGFloat] = []
9
+ var canDismiss: Bool = true
10
10
 
11
11
  // Appearance props
12
- @Published var grabberVisible: Bool = true
13
- @Published var backgroundColor: String? = nil
14
- @Published var cornerRadius: Float? = nil
12
+ var grabberVisible: Bool = true
13
+ var backgroundColor: String? = nil
14
+ var cornerRadius: Float? = nil
15
15
  }
16
16
 
17
- struct SheetAppearance: Record {
18
- @Field
19
- var grabberVisible: Bool?
20
-
21
- @Field
22
- var backgroundColor: String?
23
-
24
- @Field
25
- var cornerRadius: Float?
17
+ protocol RNToolsSheetsViewDelegate: AnyObject {
18
+ func handleSheetStateChange(index: Int)
19
+ func handleSheetDismissed()
20
+ func handleSheetCanDismiss() -> Bool
26
21
  }
27
22
 
28
- public class RNToolsSheetsView: ExpoView {
23
+ public class RNToolsSheetsView: ExpoView, RNToolsSheetsViewDelegate {
29
24
  public var props = SheetProps()
25
+
30
26
  var onDismiss = EventDispatcher()
31
27
  var onStateChange = EventDispatcher()
28
+ var onDismissPrevented = EventDispatcher()
32
29
 
33
- var touchHandler: RCTTouchHandler?
34
-
35
- private lazy var sheetVC = SheetInternalViewController()
36
-
37
- lazy var hostingController = UIHostingController(
38
- rootView: ContentView(
39
- props: props, sheetVC: sheetVC, onDismiss: onDismiss,
40
- onStateChange: onStateChange))
30
+ private lazy var sheetVC = SheetViewController()
41
31
 
42
32
  required init(appContext: AppContext? = nil) {
43
33
  super.init(appContext: appContext)
44
34
 
45
- if let bridge = appContext?.reactBridge {
46
- sheetVC.bridge = bridge
35
+ sheetVC.appContext = appContext
36
+ sheetVC.delegate = self
37
+ }
38
+
39
+ deinit {
40
+ sheetVC.cleanup()
41
+ }
42
+
43
+ func updateSnapPoints(_ snapPoints: [CGFloat]) {
44
+ props.snapPoints = snapPoints
45
+ if props.isOpen {
46
+ sheetVC.updateSheetConfiguration(
47
+ openTo: props.initialIndex,
48
+ snapPoints: props.snapPoints,
49
+ grabberVisible: props.grabberVisible,
50
+ backgroundColor: props.backgroundColor,
51
+ cornerRadius: props.cornerRadius
52
+ )
47
53
  }
54
+ }
48
55
 
49
- hostingController.view.autoresizingMask = [
50
- .flexibleWidth, .flexibleHeight,
51
- ]
52
- hostingController.view.backgroundColor = UIColor.clear
56
+ func updateIsOpen(_ isOpen: Bool) {
57
+ props.isOpen = isOpen
58
+ if isOpen {
59
+ sheetVC.presentSheet(
60
+ openTo: props.initialIndex,
61
+ snapPoints: props.snapPoints,
62
+ grabberVisible: props.grabberVisible,
63
+ backgroundColor: props.backgroundColor,
64
+ cornerRadius: props.cornerRadius
65
+ )
66
+ } else {
67
+ sheetVC.dismissSheet()
68
+ }
53
69
  }
54
70
 
55
- public override func layoutSubviews() {
56
- super.layoutSubviews()
57
- hostingController.view.frame = bounds
71
+ func updateInitialIndex(_ initialIndex: Int) {
72
+ props.initialIndex = initialIndex
73
+ if props.isOpen {
74
+ sheetVC.updateSheetConfiguration(
75
+ openTo: props.initialIndex,
76
+ snapPoints: props.snapPoints,
77
+ grabberVisible: props.grabberVisible,
78
+ backgroundColor: props.backgroundColor,
79
+ cornerRadius: props.cornerRadius
80
+ )
81
+ }
58
82
  }
59
83
 
60
- public override func didMoveToSuperview() {
61
- super.didMoveToSuperview()
62
- let parentViewController = findParentViewControllerOrNil()
84
+ func updateCanDismiss(_ canDismiss: Bool) {
85
+ props.canDismiss = canDismiss
86
+ }
63
87
 
64
- parentViewController.apply {
65
- $0.addChild(hostingController)
88
+ func updateAppearance(
89
+ grabberVisible: Bool,
90
+ backgroundColor: String?,
91
+ cornerRadius: Float?
92
+ ) {
93
+ props.grabberVisible = grabberVisible
94
+ props.backgroundColor = backgroundColor
95
+ props.cornerRadius = cornerRadius
96
+ if props.isOpen {
97
+ sheetVC.updateSheetConfiguration(
98
+ openTo: props.initialIndex,
99
+ snapPoints: props.snapPoints,
100
+ grabberVisible: props.grabberVisible,
101
+ backgroundColor: props.backgroundColor,
102
+ cornerRadius: props.cornerRadius
103
+ )
66
104
  }
105
+ }
67
106
 
68
- hostingController.view.layer.removeAllAnimations()
69
- hostingController.view.frame = bounds
70
- addSubview(hostingController.view)
71
- parentViewController.apply {
72
- hostingController.didMove(toParent: $0)
107
+ func handleSheetDismissed() {
108
+ onDismiss([:])
109
+ onStateChange(["type": "HIDDEN"])
110
+ }
111
+
112
+ func handleSheetStateChange(index: Int) {
113
+ onStateChange([
114
+ "type": "OPEN",
115
+ "payload": ["index": index],
116
+ ])
117
+ }
118
+
119
+ func handleSheetCanDismiss() -> Bool {
120
+ if !props.canDismiss {
121
+ onDismissPrevented([:])
73
122
  }
123
+ return props.canDismiss
74
124
  }
75
125
 
76
126
  public override func reactSubviews() -> [UIView]! {
77
127
  return []
78
128
  }
79
129
 
80
- #if RCT_NEW_ARCH_ENABLED
130
+
81
131
  public override func mountChildComponentView(
82
132
  _ childComponentView: UIView,
83
133
  index: Int
@@ -91,164 +141,136 @@ public class RNToolsSheetsView: ExpoView {
91
141
  ) {
92
142
  childComponentView.removeFromSuperview()
93
143
  }
144
+ // public override func insertReactSubview(
145
+ // _ subview: UIView!, at atIndex: Int
146
+ // ) {
147
+ // sheetVC.insertChild(subview, at: atIndex)
148
+ // }
149
+ //
150
+ // public override func removeReactSubview(_ subview: UIView!) {
151
+ // sheetVC.removeChild(subview)
152
+ // }
94
153
 
95
- #else
96
- public override func insertReactSubview(
97
- _ subview: UIView!, at atIndex: Int
98
- ) {
99
- sheetVC.insertChild(subview, at: atIndex)
100
- }
101
-
102
- public override func removeReactSubview(_ subview: UIView!) {
103
- sheetVC.removeChild(subview)
104
- }
154
+ }
105
155
 
106
- #endif
156
+ final class SheetViewController: UIViewController,
157
+ UISheetPresentationControllerDelegate
158
+ {
159
+ weak var delegate: RNToolsSheetsViewDelegate?
107
160
 
108
- }
161
+ var appContext: AppContext? {
162
+ didSet {
163
+ _ = view
164
+ }
165
+ }
109
166
 
110
- struct ContentView: View {
111
- @ObservedObject var props: SheetProps
112
- var sheetVC: SheetInternalViewController
113
- var onDismiss: EventDispatcher
114
- var onStateChange: EventDispatcher
167
+ @available(*, unavailable) required init?(coder: NSCoder) { fatalError() }
115
168
 
116
- @State private var selectedDetent: PresentationDetent = .height(400.0)
117
- @State private var lastHeight: CGFloat = 0
118
- @State private var isDragging = false
119
- @State private var settleTimer: Timer?
169
+ init() {
170
+ super.init(nibName: nil, bundle: nil)
171
+ view.backgroundColor = .white
172
+ }
120
173
 
121
- private var detents: [PresentationDetent] {
122
- props.snapPoints.map { .height(CGFloat($0)) }
174
+ override func loadView() {
175
+ self.view = UIView()
176
+ RNToolsTouchHandlerHelper.createAndAttachTouchHandler(for: self.view)
123
177
  }
178
+
124
179
 
125
- private func detent(for index: Int?) -> PresentationDetent {
126
- guard
127
- let i = index,
128
- detents.indices.contains(i)
129
- else { return detents.first! }
130
- return detents[i]
180
+ deinit {
181
+ overlayWindow = nil
131
182
  }
132
183
 
133
- private func upperSnapIndex(
134
- for height: CGFloat,
135
- snapPoints: [Int]
136
- ) -> Int {
137
- guard !snapPoints.isEmpty else { return 0 }
184
+ private let detentTag = UUID().uuidString
185
+
186
+ private var overlayWindow: UIWindow?
187
+
188
+ func presentSheet(
189
+ openTo index: Int = 0,
190
+ snapPoints: [CGFloat],
191
+ grabberVisible: Bool,
192
+ backgroundColor: String?,
193
+ cornerRadius: Float?
194
+ ) {
195
+ guard overlayWindow == nil else {
196
+ updateSheetConfiguration(
197
+ openTo: index,
198
+ snapPoints: snapPoints,
199
+ grabberVisible: grabberVisible,
200
+ backgroundColor: backgroundColor,
201
+ cornerRadius: cornerRadius
202
+ )
203
+ return
204
+ }
205
+
206
+ modalPresentationStyle = .pageSheet
207
+
208
+ updateSheetConfiguration(
209
+ openTo: index,
210
+ snapPoints: snapPoints,
211
+ grabberVisible: grabberVisible,
212
+ backgroundColor: backgroundColor,
213
+ cornerRadius: cornerRadius
214
+ )
215
+
216
+ let w = UIWindow(frame: UIScreen.main.bounds)
217
+ w.windowLevel = .statusBar + 2
218
+ w.rootViewController = UIViewController()
219
+ w.makeKeyAndVisible()
138
220
 
139
- let sorted = snapPoints.sorted()
140
- if let i = sorted.firstIndex(where: { CGFloat($0) >= height }) {
141
- return i
221
+ overlayWindow = w
222
+
223
+ let host = UIViewController()
224
+ host.modalPresentationStyle = .overFullScreen
225
+ host.view.backgroundColor = .clear
226
+
227
+ w.rootViewController?.present(host, animated: false) {
228
+ host.present(self, animated: true) { [weak self] in
229
+ self?.emitInitialOpenState(requestedIndex: index)
230
+ }
142
231
  }
143
- return sorted.count - 1
144
232
  }
145
233
 
146
- var body: some View {
147
-
148
- Color.clear
149
- .sheet(
150
- isPresented: $props.isOpen,
151
- onDismiss: {
152
- onDismiss([:])
153
- onStateChange(["type": "HIDDEN"])
154
- }
155
- ) {
156
- SheetInternalVCRepresentable(controller: sheetVC)
157
- .background(
158
- GeometryReader { geometry in
159
- Color.clear
160
- .onChange(of: geometry.size.height) {
161
- newHeight in
162
- if abs(newHeight - lastHeight) > 2 {
163
- if !isDragging {
164
- isDragging = true
165
- onStateChange(["type": "DRAGGING"])
166
- }
167
-
168
- settleTimer?.invalidate()
169
- settleTimer = Timer.scheduledTimer(
170
- withTimeInterval: 0.15,
171
- repeats: false
172
- ) { _ in
173
- isDragging = false
174
- onStateChange(["type": "SETTLING"])
175
-
176
- DispatchQueue.main.asyncAfter(
177
- deadline: .now() + 0.15
178
- ) {
179
- let idx = upperSnapIndex(
180
- for: newHeight,
181
- snapPoints: props.snapPoints
182
- )
183
- onStateChange([
184
- "type": "OPEN",
185
- "payload": ["index": idx],
186
- ])
187
- }
188
- }
189
- }
190
-
191
- lastHeight = newHeight
192
- }
193
- }
194
- )
195
- .presentationBackground16_4(
196
- props.backgroundColor != nil
197
- ? Color(hex: props.backgroundColor!) : Color.white
198
- )
199
- .presentationCornerRadius16_4(
200
- props.cornerRadius.map { CGFloat($0) }
201
- )
202
- .presentationDragIndicator(
203
- props.grabberVisible ? .visible : .hidden
204
- )
205
- .presentationDetents(
206
- Set(detents),
207
- selection: $selectedDetent
208
- )
209
-
210
- .onAppear {
211
- selectedDetent = detent(for: props.openToIndex)
212
- }
234
+ func updateSheetConfiguration(
235
+ openTo index: Int = 0,
236
+ snapPoints: [CGFloat],
237
+ grabberVisible: Bool,
238
+ backgroundColor: String?,
239
+ cornerRadius: Float?
240
+ ) {
241
+ if let sheet = sheetPresentationController {
242
+ sheet.delegate = self
243
+ sheet.prefersGrabberVisible = grabberVisible
244
+ sheet.detents = makeDetents(from: snapPoints)
245
+
246
+ if let radius = cornerRadius {
247
+ sheet.preferredCornerRadius = CGFloat(radius)
213
248
  }
214
- }
215
- }
216
249
 
217
- struct SheetInternalVCRepresentable: UIViewControllerRepresentable {
218
- let controller: SheetInternalViewController
250
+ let detents = sheet.detents
251
+ if detents.indices.contains(index) {
252
+ sheet.selectedDetentIdentifier = detents[index].identifier
253
+ } else {
254
+ sheet.selectedDetentIdentifier = detents.first?.identifier
255
+ }
256
+ }
219
257
 
220
- func makeUIViewController(context: Context) -> SheetInternalViewController {
221
- controller
258
+ view.backgroundColor = UIColor(hex: backgroundColor) ?? .white
222
259
  }
223
- func updateUIViewController(
224
- _ uiViewController: SheetInternalViewController,
225
- context: Context
226
- ) {}
227
- }
228
260
 
229
- final class SheetInternalViewController: UIViewController {
230
- var bridge: RCTBridge? {
231
- didSet {
232
- touchHandler = RCTTouchHandler(bridge: bridge)
233
- self.touchHandler?.attach(to: self.view)
261
+ func dismissSheet() {
262
+ dismiss(animated: true) { [weak self] in
263
+ self?.delegate?.handleSheetDismissed()
264
+ self?.overlayWindow?.isHidden = true
265
+ self?.overlayWindow = nil
234
266
  }
235
267
  }
236
- var surfaceTouchHandler = RNTSurfaceTouchHandlerWrapper()
237
- var touchHandler: RCTTouchHandler?
238
-
239
- init() {
240
- super.init(nibName: nil, bundle: nil)
241
- view.backgroundColor = .clear
242
- }
243
-
244
- override func loadView() {
245
- self.view = UIView()
246
268
 
247
- self.surfaceTouchHandler.attach(to: self.view)
269
+ func cleanup() {
270
+ overlayWindow?.isHidden = true
271
+ overlayWindow = nil
248
272
  }
249
273
 
250
- @available(*, unavailable) required init?(coder: NSCoder) { fatalError() }
251
-
252
274
  func insertChild(_ child: UIView, at index: Int) {
253
275
  view.insertSubview(child, at: index)
254
276
 
@@ -257,64 +279,87 @@ final class SheetInternalViewController: UIViewController {
257
279
  func removeChild(_ child: UIView) {
258
280
  child.removeFromSuperview()
259
281
  }
260
- }
261
282
 
262
- extension Optional {
263
- func apply(_ fn: (Wrapped) -> Void) {
264
- if case let .some(val) = self {
265
- fn(val)
266
- }
283
+ func sheetPresentationControllerDidChangeSelectedDetentIdentifier(
284
+ _ sheetPresentationController: UISheetPresentationController
285
+ ) {
286
+ guard
287
+ let selectedID = sheetPresentationController
288
+ .selectedDetentIdentifier,
289
+ let index = sheetPresentationController.detents
290
+ .firstIndex(where: { $0.identifier == selectedID })
291
+ else { return }
292
+
293
+ delegate?.handleSheetStateChange(index: index)
267
294
  }
268
- }
269
295
 
270
- extension UIView {
271
- // Walks the responder chain to find the parent UIViewController
272
- // or null if not in a heirarchy yet
273
- func findParentViewControllerOrNil() -> UIViewController? {
274
- var nextResponder: UIResponder? = next
275
- while nextResponder != nil && nextResponder as? UIViewController == nil
276
- {
277
- nextResponder = nextResponder?.next
296
+ func presentationControllerShouldDismiss(
297
+ _ presentationController: UIPresentationController
298
+ ) -> Bool {
299
+ if let d = delegate {
300
+ return d.handleSheetCanDismiss()
278
301
  }
279
302
 
280
- return nextResponder as? UIViewController
303
+ return true
281
304
  }
282
- }
283
-
284
- extension Color {
285
- init(hex: String) {
286
- var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
287
- hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
288
-
289
- var rgb: UInt64 = 0
290
- Scanner(string: hexSanitized).scanHexInt64(&rgb)
291
305
 
292
- let red = Double((rgb & 0xFF0000) >> 16) / 255.0
293
- let green = Double((rgb & 0x00FF00) >> 8) / 255.0
294
- let blue = Double(rgb & 0x0000FF) / 255.0
295
-
296
- self.init(red: red, green: green, blue: blue)
306
+ func presentationControllerDidDismiss(
307
+ _ presentationController: UIPresentationController
308
+ ) {
309
+ delegate?.handleSheetDismissed()
310
+ cleanup()
297
311
  }
298
- }
299
312
 
313
+ private func emitInitialOpenState(requestedIndex: Int) {
314
+ guard let sheet = sheetPresentationController else { return }
300
315
 
301
- extension View {
302
- @ViewBuilder
303
- func presentationBackground16_4(_ color: Color?) -> some View {
304
- if #available(iOS 16.4, *) {
305
- self.presentationBackground(color ?? .white)
316
+ if
317
+ let selectedID = sheet.selectedDetentIdentifier,
318
+ let index = sheet.detents.firstIndex(where: { $0.identifier == selectedID })
319
+ {
320
+ delegate?.handleSheetStateChange(index: index)
321
+ return
322
+ }
323
+
324
+ let fallbackIndex: Int
325
+ if sheet.detents.indices.contains(requestedIndex) {
326
+ fallbackIndex = requestedIndex
306
327
  } else {
307
- self
328
+ fallbackIndex = 0
308
329
  }
330
+ delegate?.handleSheetStateChange(index: fallbackIndex)
309
331
  }
310
332
 
311
- @ViewBuilder
312
- func presentationCornerRadius16_4(_ radius: CGFloat?) -> some View {
313
- if #available(iOS 16.4, *) {
314
- self.presentationCornerRadius(radius)
315
- } else {
316
- self
333
+ private func makeDetents(from points: [CGFloat])
334
+ -> [UISheetPresentationController.Detent]
335
+ {
336
+ guard !points.isEmpty else { return [.large()] }
337
+
338
+ return points.enumerated().map { idx, raw in
339
+ .custom(identifier: .init("\(detentTag)_\(idx)")) { _ in
340
+ return raw
341
+ }
317
342
  }
318
343
  }
344
+
319
345
  }
320
346
 
347
+ extension UIColor {
348
+ convenience init?(hex: String?) {
349
+ guard let hex, !hex.isEmpty else { return nil }
350
+
351
+ var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
352
+ hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
353
+
354
+ guard hexSanitized.count == 6 else { return nil }
355
+
356
+ var rgb: UInt64 = 0
357
+ guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil }
358
+
359
+ let red = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
360
+ let green = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
361
+ let blue = CGFloat(rgb & 0x0000FF) / 255.0
362
+
363
+ self.init(red: red, green: green, blue: blue, alpha: 1.0)
364
+ }
365
+ }
@@ -0,0 +1,15 @@
1
+ // RNToolsTouchHandlerHelper.h
2
+
3
+ #import <Foundation/Foundation.h>
4
+ #import <UIKit/UIKit.h>
5
+
6
+ NS_ASSUME_NONNULL_BEGIN
7
+
8
+ @interface RNToolsTouchHandlerHelper : NSObject
9
+
10
+ + (nullable UIGestureRecognizer *)createAndAttachTouchHandlerForView:(UIView *)view;
11
+ + (void)detachTouchHandler:(nullable UIGestureRecognizer *)handler fromView:(UIView *)view;
12
+
13
+ @end
14
+
15
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,31 @@
1
+ // RNToolsTouchHandlerHelper.mm
2
+
3
+ #import "RNToolsTouchHandlerHelper.h"
4
+ #import <React/RCTSurfaceTouchHandler.h>
5
+
6
+ @implementation RNToolsTouchHandlerHelper
7
+
8
+ + (nullable UIGestureRecognizer *)createAndAttachTouchHandlerForView:(UIView *)view {
9
+ for (UIGestureRecognizer *recognizer in [view.gestureRecognizers copy]) {
10
+ if ([recognizer isKindOfClass:[RCTSurfaceTouchHandler class]]) {
11
+ return nil;
12
+ }
13
+ }
14
+
15
+ RCTSurfaceTouchHandler *touchHandler = [[RCTSurfaceTouchHandler alloc] init];
16
+ [touchHandler attachToView:view];
17
+ return touchHandler;
18
+ }
19
+
20
+ + (void)detachTouchHandler:(nullable UIGestureRecognizer *)handler fromView:(UIView *)view {
21
+ if (!handler) {
22
+ return;
23
+ }
24
+
25
+ if ([handler isKindOfClass:[RCTSurfaceTouchHandler class]]) {
26
+ RCTSurfaceTouchHandler *touchHandler = (RCTSurfaceTouchHandler *)handler;
27
+ [touchHandler detachFromView:view];
28
+ }
29
+ }
30
+
31
+ @end
@@ -0,0 +1,9 @@
1
+ export function requireNativeViewManager() {
2
+ return "div";
3
+ }
4
+
5
+ export function requireNativeModule() {
6
+ return {
7
+ addListener: () => ({ remove: () => {} }),
8
+ };
9
+ }
package/package.json CHANGED
@@ -1,18 +1,12 @@
1
1
  {
2
2
  "name": "@rn-tools/sheets",
3
- "version": "0.1.4",
4
- "description": "My new module",
3
+ "version": "3.0.2",
4
+ "description": "A React Native library for creating and managing native sheets in Expo applications.",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
7
- "build": "expo-module build",
8
- "clean": "expo-module clean",
9
- "lint": "expo-module lint",
10
- "test": "expo-module test",
11
- "prepare": "expo-module prepare",
12
- "prepublishOnly": "expo-module prepublishOnly",
13
- "expo-module": "expo-module",
14
7
  "open:ios": "xed example/ios",
15
- "open:android": "open -a \"Android Studio\" example/android"
8
+ "open:android": "open -a \"Android Studio\" example/android",
9
+ "test": "NODE_OPTIONS='--no-experimental-detect-module' vitest"
16
10
  },
17
11
  "keywords": [
18
12
  "react-native",
@@ -28,10 +22,12 @@
28
22
  "license": "MIT",
29
23
  "homepage": "https://github.com/ajsmth/rn-tools#readme",
30
24
  "devDependencies": {
31
- "@types/react": "~18.3.12",
32
- "expo": "~52.0.0",
33
- "expo-module-scripts": "^4.0.3",
34
- "react-native": "0.76.0"
25
+ "@testing-library/react": "^14.2.1",
26
+ "@types/react": "18.3.12",
27
+ "vitest": "^1.6.0"
28
+ },
29
+ "dependencies": {
30
+ "@rn-tools/core": "3.0.2"
35
31
  },
36
32
  "peerDependencies": {
37
33
  "expo": "*",