one 2.6.21 → 2.7.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.
Files changed (56) hide show
  1. package/android/src/main/java/dev/onejs/onenative/OneNativeCameraManager.kt +24 -0
  2. package/android/src/main/java/dev/onejs/onenative/OneNativeCameraView.kt +197 -2
  3. package/dist/cjs/platform/camera/index.cjs +3 -2
  4. package/dist/cjs/platform/camera/index.native.cjs +89 -3
  5. package/dist/cjs/platform/camera/index.native.js +89 -3
  6. package/dist/cjs/platform/camera/index.native.js.map +1 -1
  7. package/dist/cjs/platform/specs/OneNativeCameraNativeComponent.cjs +37 -21
  8. package/dist/cjs/platform/specs/OneNativeCameraNativeComponent.native.cjs +37 -21
  9. package/dist/cjs/platform/specs/OneNativeCameraNativeComponent.native.js +37 -21
  10. package/dist/cjs/platform/specs/OneNativeCameraNativeComponent.native.js.map +3 -3
  11. package/dist/cjs/router/router.cjs +4 -1
  12. package/dist/cjs/useHeaderHeight.native.cjs +5 -23
  13. package/dist/cjs/useHeaderHeight.native.js +5 -23
  14. package/dist/cjs/useHeaderHeight.native.js.map +1 -1
  15. package/dist/esm/platform/camera/index.mjs +4 -2
  16. package/dist/esm/platform/camera/index.mjs.map +1 -1
  17. package/dist/esm/platform/camera/index.native.js +90 -4
  18. package/dist/esm/platform/camera/index.native.js.map +1 -1
  19. package/dist/esm/platform/specs/OneNativeCameraNativeComponent.mjs +25 -9
  20. package/dist/esm/platform/specs/OneNativeCameraNativeComponent.mjs.map +3 -3
  21. package/dist/esm/platform/specs/OneNativeCameraNativeComponent.native.js +25 -9
  22. package/dist/esm/platform/specs/OneNativeCameraNativeComponent.native.js.map +3 -3
  23. package/dist/esm/router/router.mjs +5 -2
  24. package/dist/esm/router/router.mjs.map +1 -1
  25. package/dist/esm/useHeaderHeight.native.js +3 -21
  26. package/dist/esm/useHeaderHeight.native.js.map +1 -1
  27. package/ios/Nitro/HybridOneImageManipulator.swift +1 -1
  28. package/ios/OneNativeCameraComponentView.mm +28 -0
  29. package/ios/OneNativeCameraView.swift +163 -0
  30. package/package.json +27 -27
  31. package/src/index.ts +3 -0
  32. package/src/platform/camera/index.native.tsx +116 -12
  33. package/src/platform/camera/index.tsx +15 -4
  34. package/src/platform/camera/types.ts +22 -0
  35. package/src/platform/index.native.ts +4 -0
  36. package/src/platform/index.ts +4 -0
  37. package/src/platform/specs/OneNativeCameraNativeComponent.ts +35 -2
  38. package/src/router/router.ts +7 -4
  39. package/src/useHeaderHeight.native.ts +7 -0
  40. package/types/index.d.ts +1 -1
  41. package/types/index.d.ts.map +1 -1
  42. package/types/platform/camera/index.d.ts +3 -3
  43. package/types/platform/camera/index.d.ts.map +1 -1
  44. package/types/platform/camera/index.native.d.ts +3 -3
  45. package/types/platform/camera/index.native.d.ts.map +1 -1
  46. package/types/platform/camera/types.d.ts +18 -0
  47. package/types/platform/camera/types.d.ts.map +1 -1
  48. package/types/platform/index.d.ts +1 -1
  49. package/types/platform/index.d.ts.map +1 -1
  50. package/types/platform/index.native.d.ts +1 -1
  51. package/types/platform/index.native.d.ts.map +1 -1
  52. package/types/platform/specs/OneNativeCameraNativeComponent.d.ts +18 -2
  53. package/types/platform/specs/OneNativeCameraNativeComponent.d.ts.map +1 -1
  54. package/types/router/router.d.ts.map +1 -1
  55. package/types/useHeaderHeight.native.d.ts +2 -0
  56. package/types/useHeaderHeight.native.d.ts.map +1 -0
@@ -13,9 +13,28 @@ private final class CameraMetadataDelegate: NSObject, AVCaptureMetadataOutputObj
13
13
  }
14
14
  }
15
15
 
16
+ // one capture in flight; AVCapturePhotoOutput holds its delegate weakly.
17
+ private final class CameraPhotoDelegate: NSObject, AVCapturePhotoCaptureDelegate {
18
+ let finish: (AVCapturePhoto?, Error?) -> Void
19
+
20
+ init(finish: @escaping (AVCapturePhoto?, Error?) -> Void) {
21
+ self.finish = finish
22
+ }
23
+
24
+ func photoOutput(
25
+ _ output: AVCapturePhotoOutput,
26
+ didFinishProcessingPhoto photo: AVCapturePhoto,
27
+ error: Error?
28
+ ) {
29
+ finish(photo, error)
30
+ }
31
+ }
32
+
16
33
  @objcMembers public final class OneNativeCameraView: UIView {
17
34
  public var onStateChange: ((String) -> Void)?
18
35
  public var onCodeScanned: ((String, String) -> Void)?
36
+ // requestId, uri, width, height, base64, error ("" on success).
37
+ public var onPicture: ((Int, String, Int, Int, String, String) -> Void)?
19
38
 
20
39
  public override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
21
40
 
@@ -32,6 +51,10 @@ private final class CameraMetadataDelegate: NSObject, AVCaptureMetadataOutputObj
32
51
  private let sessionQueue = DispatchQueue(label: "dev.one.camera.capture")
33
52
  private var session: AVCaptureSession?
34
53
  private var metadataOutput: AVCaptureMetadataOutput?
54
+ // owned by sessionQueue.
55
+ private var photoOutput: AVCapturePhotoOutput?
56
+ private var captureDevice: AVCaptureDevice?
57
+ private var photoDelegates: [Int: CameraPhotoDelegate] = [:]
35
58
  private let metadataDelegate = CameraMetadataDelegate()
36
59
  private var sessionObservers: [NSObjectProtocol] = []
37
60
  private var rotationCoordinator: AVCaptureDevice.RotationCoordinator?
@@ -41,6 +64,9 @@ private final class CameraMetadataDelegate: NSObject, AVCaptureMetadataOutputObj
41
64
  private var active = false
42
65
  private var facing = "back"
43
66
  private var codeTypes: [String] = []
67
+ private var flash = "off"
68
+ private var torch = false
69
+ private var zoom: Double = 0
44
70
  private var generation = 0
45
71
  private var lastState = ""
46
72
  private var activeObserver: NSObjectProtocol?
@@ -82,13 +108,132 @@ private final class CameraMetadataDelegate: NSObject, AVCaptureMetadataOutputObj
82
108
  refresh(reconfigure: true)
83
109
  }
84
110
 
111
+ // flash, torch and zoom change the running device without restarting the session.
112
+ public func configureCapture(_ flash: String, torch: Bool, zoom: Double) {
113
+ guard self.flash != flash || self.torch != torch || self.zoom != zoom else { return }
114
+ self.flash = flash
115
+ self.torch = torch
116
+ self.zoom = zoom
117
+ sessionQueue.async { self.applyDeviceSettings(torch: torch, zoom: zoom) }
118
+ }
119
+
85
120
  public func reset() {
86
121
  active = false
87
122
  facing = "back"
88
123
  codeTypes = []
124
+ flash = "off"
125
+ torch = false
126
+ zoom = 0
89
127
  refresh()
90
128
  }
91
129
 
130
+ private func applyDeviceSettings(torch: Bool, zoom: Double) {
131
+ guard let device = captureDevice, (try? device.lockForConfiguration()) != nil else { return }
132
+ defer { device.unlockForConfiguration() }
133
+ let torchMode: AVCaptureDevice.TorchMode = torch ? .on : .off
134
+ if device.hasTorch, device.isTorchModeSupported(torchMode) { device.torchMode = torchMode }
135
+ let low = device.minAvailableVideoZoomFactor
136
+ let high = device.maxAvailableVideoZoomFactor
137
+ device.videoZoomFactor = low + CGFloat(zoom) * (high - low)
138
+ }
139
+
140
+ public func takePicture(_ requestId: Int, quality: Double, base64: Bool) {
141
+ let fail = { [weak self] (code: String, message: String) in
142
+ self?.onPicture?(requestId, "", 0, 0, "", "\(code): \(message)")
143
+ }
144
+ guard sessionReady, let coordinator = rotationCoordinator else {
145
+ let state = lastState.isEmpty ? "waiting" : lastState
146
+ fail("E_CAMERA_NOT_READY", "CameraView is not ready to take a picture (state: \(state))")
147
+ return
148
+ }
149
+ let angle = coordinator.videoRotationAngleForHorizonLevelCapture
150
+ let flashName = flash
151
+ let current = generation
152
+ sessionQueue.async { [weak self] in
153
+ guard let self, let output = self.photoOutput,
154
+ output.availablePhotoCodecTypes.contains(.jpeg)
155
+ else {
156
+ DispatchQueue.main.async { fail("E_CAMERA_NOT_READY", "CameraView has no photo output") }
157
+ return
158
+ }
159
+ let settings = AVCapturePhotoSettings(format: [
160
+ AVVideoCodecKey: AVVideoCodecType.jpeg,
161
+ AVVideoCompressionPropertiesKey: [AVVideoQualityKey: quality],
162
+ ])
163
+ let flashMode: AVCaptureDevice.FlashMode =
164
+ flashName == "on" ? .on : flashName == "auto" ? .auto : .off
165
+ if output.supportedFlashModes.contains(flashMode) { settings.flashMode = flashMode }
166
+ if let connection = output.connection(with: .video),
167
+ connection.isVideoRotationAngleSupported(angle)
168
+ {
169
+ connection.videoRotationAngle = angle
170
+ }
171
+ let delegate = CameraPhotoDelegate { [weak self] photo, error in
172
+ guard let self else { return }
173
+ // photo callbacks arrive on the output's private queue.
174
+ self.sessionQueue.async { self.photoDelegates[requestId] = nil }
175
+ let result = Self.savePicture(photo: photo, error: error, base64: base64)
176
+ DispatchQueue.main.async {
177
+ guard self.generation == current else {
178
+ fail("E_CAMERA_INACTIVE", "CameraView stopped before the picture was taken")
179
+ return
180
+ }
181
+ switch result {
182
+ case .success(let picture):
183
+ self.onPicture?(requestId, picture.uri, picture.width, picture.height, picture.base64, "")
184
+ case .failure(let failure):
185
+ fail("E_CAMERA_CAPTURE", failure.message)
186
+ }
187
+ }
188
+ }
189
+ self.photoDelegates[requestId] = delegate
190
+ output.capturePhoto(with: settings, delegate: delegate)
191
+ }
192
+ }
193
+
194
+ private struct SavedPicture {
195
+ let uri: String
196
+ let width: Int
197
+ let height: Int
198
+ let base64: String
199
+ }
200
+
201
+ private struct CaptureFailure: Error {
202
+ let message: String
203
+ }
204
+
205
+ // writes the jpeg to caches/Camera and reports its displayed size: exif
206
+ // orientations 5 to 8 are rotated a quarter turn, so their sides swap.
207
+ private static func savePicture(
208
+ photo: AVCapturePhoto?, error: Error?, base64: Bool
209
+ ) -> Result<SavedPicture, CaptureFailure> {
210
+ if let error { return .failure(CaptureFailure(message: error.localizedDescription)) }
211
+ guard let data = photo?.fileDataRepresentation(),
212
+ let source = CGImageSourceCreateWithData(data as CFData, nil),
213
+ let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any],
214
+ let pixelWidth = properties[kCGImagePropertyPixelWidth] as? Int,
215
+ let pixelHeight = properties[kCGImagePropertyPixelHeight] as? Int
216
+ else { return .failure(CaptureFailure(message: "The camera returned no image data")) }
217
+ let orientation = properties[kCGImagePropertyOrientation] as? Int ?? 1
218
+ let quarterTurn = (5...8).contains(orientation)
219
+ do {
220
+ let directory = try FileManager.default
221
+ .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
222
+ .appendingPathComponent("Camera", isDirectory: true)
223
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
224
+ let file = directory.appendingPathComponent("\(UUID().uuidString).jpg")
225
+ try data.write(to: file, options: .atomic)
226
+ return .success(SavedPicture(
227
+ uri: file.absoluteString,
228
+ width: quarterTurn ? pixelHeight : pixelWidth,
229
+ height: quarterTurn ? pixelWidth : pixelHeight,
230
+ base64: base64 ? data.base64EncodedString() : ""
231
+ ))
232
+ } catch {
233
+ return .failure(CaptureFailure(message: error.localizedDescription))
234
+ }
235
+ }
236
+
92
237
  private func emitState(_ state: String) {
93
238
  guard lastState != state else { return }
94
239
  lastState = state
@@ -115,6 +260,8 @@ private final class CameraMetadataDelegate: NSObject, AVCaptureMetadataOutputObj
115
260
  sessionQueue.async {
116
261
  self.session?.stopRunning()
117
262
  self.session = nil
263
+ self.photoOutput = nil
264
+ self.captureDevice = nil
118
265
  }
119
266
  emitState("inactive")
120
267
  return
@@ -125,6 +272,8 @@ private final class CameraMetadataDelegate: NSObject, AVCaptureMetadataOutputObj
125
272
  sessionQueue.async {
126
273
  self.session?.stopRunning()
127
274
  self.session = nil
275
+ self.photoOutput = nil
276
+ self.captureDevice = nil
128
277
  }
129
278
  emitState("permission-required")
130
279
  return
@@ -142,6 +291,8 @@ private final class CameraMetadataDelegate: NSObject, AVCaptureMetadataOutputObj
142
291
  guard let self else { return }
143
292
  self.session?.stopRunning()
144
293
  self.session = nil
294
+ self.photoOutput = nil
295
+ self.captureDevice = nil
145
296
  guard let device = AVCaptureDevice.default(
146
297
  .builtInWideAngleCamera, for: .video, position: position
147
298
  ) else {
@@ -178,7 +329,16 @@ private final class CameraMetadataDelegate: NSObject, AVCaptureMetadataOutputObj
178
329
  metadata.setMetadataObjectsDelegate(self.metadataDelegate, queue: self.sessionQueue)
179
330
  output = metadata
180
331
  }
332
+ let photo = AVCapturePhotoOutput()
333
+ guard next.canAddOutput(photo) else {
334
+ next.commitConfiguration()
335
+ self.emitState("failed", generation: current)
336
+ return
337
+ }
338
+ next.addOutput(photo)
181
339
  next.commitConfiguration()
340
+ self.photoOutput = photo
341
+ self.captureDevice = device
182
342
 
183
343
  var readyState = "ready"
184
344
  if let output {
@@ -211,8 +371,11 @@ private final class CameraMetadataDelegate: NSObject, AVCaptureMetadataOutputObj
211
371
  }
212
372
  self.observeSession(next)
213
373
  let queue = self.sessionQueue
374
+ let torch = self.torch
375
+ let zoom = self.zoom
214
376
  queue.async { [weak self] in
215
377
  next.startRunning()
378
+ self?.applyDeviceSettings(torch: torch, zoom: zoom)
216
379
  let running = next.isRunning
217
380
  DispatchQueue.main.async { [weak self] in
218
381
  guard let self, self.generation == current else { return }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "one",
3
- "version": "2.6.21",
3
+ "version": "2.7.0",
4
4
  "description": "One is a React framework for web, iOS, and Android apps.",
5
5
  "license": "BSD-3-Clause",
6
6
  "sideEffects": [
@@ -231,23 +231,23 @@
231
231
  "@react-native-community/cli-platform-android": "^20.2.0",
232
232
  "@react-native-community/cli-platform-ios": "^20.2.0",
233
233
  "@radix-ui/react-slot": "^1.0.2",
234
- "@react-navigation/bottom-tabs": "8.0.0-alpha.56",
235
- "@react-navigation/elements": "3.0.0-alpha.54",
236
- "@react-navigation/native-stack": "8.0.0-alpha.58",
237
- "@react-navigation/routers": "8.0.0-alpha.20",
234
+ "@react-navigation/bottom-tabs": "8.0.0-alpha.57",
235
+ "@react-navigation/elements": "3.0.0-alpha.55",
236
+ "@react-navigation/native-stack": "8.0.0-alpha.59",
237
+ "@react-navigation/routers": "8.0.0-alpha.21",
238
238
  "@ungap/structured-clone": "^1.2.0",
239
- "@vxrn/color-scheme": "2.6.21",
240
- "@vxrn/compiler": "2.6.21",
241
- "@vxrn/resolve": "2.6.21",
242
- "@vxrn/tslib-lite": "2.6.21",
243
- "@vxrn/use-isomorphic-layout-effect": "2.6.21",
244
- "@vxrn/utils": "2.6.21",
245
- "@vxrn/vite-plugin-metro": "2.6.21",
239
+ "@vxrn/color-scheme": "2.7.0",
240
+ "@vxrn/compiler": "2.7.0",
241
+ "@vxrn/resolve": "2.7.0",
242
+ "@vxrn/tslib-lite": "2.7.0",
243
+ "@vxrn/use-isomorphic-layout-effect": "2.7.0",
244
+ "@vxrn/utils": "2.7.0",
245
+ "@vxrn/vite-plugin-metro": "2.7.0",
246
246
  "babel-dead-code-elimination": "1.0.10",
247
247
  "babel-plugin-module-resolver": "^5.0.2",
248
248
  "citty": "^0.1.6",
249
249
  "core-js": "^3.38.1",
250
- "create-vxrn": "2.6.21",
250
+ "create-vxrn": "2.7.0",
251
251
  "escape-string-regexp": "^5.0.0",
252
252
  "fast-deep-equal": "^3.1.3",
253
253
  "fast-glob": "^3.3.3",
@@ -269,7 +269,7 @@
269
269
  "tsconfig-paths": "^4",
270
270
  "use-latest-callback": "^0.2.3",
271
271
  "vite": "^8.2.2",
272
- "vxrn": "2.6.21",
272
+ "vxrn": "2.7.0",
273
273
  "web-streams-polyfill": "^4.3.0",
274
274
  "ws": "^8.21.0",
275
275
  "xxhashjs": "^0.2.2"
@@ -277,15 +277,15 @@
277
277
  "peerDependencies": {
278
278
  "@callstack/liquid-glass": ">=0.6.0",
279
279
  "@react-native/babel-preset": "^0.87.1",
280
- "@react-navigation/core": "8.0.0-alpha.40",
281
- "@react-navigation/drawer": "8.0.0-alpha.57",
282
- "@react-navigation/native": "8.0.0-alpha.50",
280
+ "@react-navigation/core": "8.0.0-alpha.41",
281
+ "@react-navigation/drawer": "8.0.0-alpha.58",
282
+ "@react-navigation/native": "8.0.0-alpha.51",
283
283
  "react": "^19.2.3",
284
284
  "react-dom": "^19.2.3",
285
285
  "react-native": "^0.87.0",
286
286
  "react-native-gesture-handler": ">=3.0.0",
287
287
  "react-native-reanimated": ">=4.0.0",
288
- "react-native-screens": ">=4.25.0",
288
+ "react-native-screens": ">=4.30.0",
289
289
  "react-native-web": "^0.21.2",
290
290
  "react-native-worklets": ">=0.12.2",
291
291
  "sharp": "^0.35.5",
@@ -325,19 +325,19 @@
325
325
  }
326
326
  },
327
327
  "devDependencies": {
328
- "@tamagui/sheet": "3.0.9",
328
+ "@tamagui/sheet": "3.0.10",
329
329
  "@callstack/liquid-glass": "^0.8.1",
330
330
  "@react-native/babel-plugin-codegen": "0.87.1",
331
331
  "@react-native/codegen": "^0.87.1",
332
- "@react-navigation/core": "8.0.0-alpha.40",
333
- "@react-navigation/drawer": "8.0.0-alpha.57",
334
- "@react-navigation/native": "8.0.0-alpha.50",
335
- "@tamagui/build": "3.0.9",
332
+ "@react-navigation/core": "8.0.0-alpha.41",
333
+ "@react-navigation/drawer": "8.0.0-alpha.58",
334
+ "@react-navigation/native": "8.0.0-alpha.51",
335
+ "@tamagui/build": "3.0.10",
336
336
  "@types/node": "^24.10.0",
337
337
  "@types/react-dom": "^19.2.2",
338
338
  "@types/react-test-renderer": "^19.1.0",
339
339
  "@types/xxhashjs": "^0.2.4",
340
- "@vxrn/emitter": "2.6.21",
340
+ "@vxrn/emitter": "2.7.0",
341
341
  "depcheck": "^1.4.7",
342
342
  "esbuild": "^0.25.12",
343
343
  "immer": "^10.1.1",
@@ -347,7 +347,7 @@
347
347
  "react-native": "0.87.1",
348
348
  "react-native-gesture-handler": "~3.3.0",
349
349
  "react-native-reanimated": "~4.6.0",
350
- "react-native-screens": "~4.27.0",
350
+ "react-native-screens": "~4.30.0",
351
351
  "react-native-web": "^0.21.2",
352
352
  "react-native-worklets": "~0.12.2",
353
353
  "react-test-renderer": "^19.2.0",
@@ -356,7 +356,7 @@
356
356
  "typescript": "7.0.2",
357
357
  "vitest": "^4.1.0",
358
358
  "yoga-layout": "^3.2.1",
359
- "@tamagui/web": "3.0.9"
359
+ "@tamagui/web": "3.0.10"
360
360
  },
361
361
  "publishConfig": {
362
362
  "access": "public"
@@ -513,7 +513,7 @@
513
513
  }
514
514
  }
515
515
  },
516
- "releaseSourceCommit": "e08c1bc6bc698fb2ae68b3d47a52b39a764657ed",
516
+ "releaseSourceCommit": "c3cd8830a7de8c60de78420b946a6ee56ef01ea0",
517
517
  "repository": {
518
518
  "type": "git",
519
519
  "url": "git+https://github.com/onejs/one.git",
package/src/index.ts CHANGED
@@ -65,6 +65,9 @@ export type {
65
65
  ComposeSnackbarShowOptions,
66
66
  ComposeSnackbarResult,
67
67
  ComposeModalBottomSheetRef,
68
+ CameraCapturedPicture,
69
+ CameraPictureOptions,
70
+ CameraViewRef,
68
71
  } from './platform'
69
72
  export type { OneRouter } from './interfaces/router'
70
73
 
@@ -1,8 +1,43 @@
1
+ import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
1
2
  import { Platform } from 'react-native'
2
- import NativeCamera from '../specs/OneNativeCameraNativeComponent'
3
- import type { CameraCodeType, CameraState, CameraViewProps } from './types'
3
+ import NativeCamera, { Commands } from '../specs/OneNativeCameraNativeComponent'
4
+ import type {
5
+ CameraCapturedPicture,
6
+ CameraCodeType,
7
+ CameraState,
8
+ CameraViewProps,
9
+ CameraViewRef,
10
+ } from './types'
4
11
 
5
- export type { CameraCode, CameraCodeType, CameraFacing, CameraState, CameraViewProps } from './types'
12
+ export type {
13
+ CameraCapturedPicture,
14
+ CameraCode,
15
+ CameraCodeType,
16
+ CameraFacing,
17
+ CameraFlashMode,
18
+ CameraPictureOptions,
19
+ CameraState,
20
+ CameraViewProps,
21
+ CameraViewRef,
22
+ } from './types'
23
+
24
+ type PendingPicture = {
25
+ resolve: (picture: CameraCapturedPicture) => void
26
+ reject: (error: Error) => void
27
+ base64: boolean
28
+ }
29
+
30
+ let nextRequestId = 1
31
+
32
+ function codedError(code: string, message: string) {
33
+ return Object.assign(new Error(message), { code })
34
+ }
35
+
36
+ // native failures arrive as "<code>: <message>", the same form nitro rejects with.
37
+ function nativeError(value: string) {
38
+ const coded = /^((?:E|ERR)_[A-Z0-9_]+): ([\s\S]*)$/.exec(value)
39
+ return coded ? codedError(coded[1], coded[2]) : new Error(value)
40
+ }
6
41
 
7
42
  function cameraState(value: string): CameraState {
8
43
  switch (value) {
@@ -41,14 +76,20 @@ function codeType(value: string): CameraCodeType {
41
76
  return value
42
77
  }
43
78
 
44
- export function CameraView({
45
- active = true,
46
- facing = 'back',
47
- codeTypes = ['qr'],
48
- onStateChange,
49
- onCodeScanned,
50
- ...viewProps
51
- }: CameraViewProps) {
79
+ export const CameraView = forwardRef<CameraViewRef, CameraViewProps>(function CameraView(
80
+ {
81
+ active = true,
82
+ facing = 'back',
83
+ codeTypes = ['qr'],
84
+ flash = 'off',
85
+ enableTorch = false,
86
+ zoom = 0,
87
+ onStateChange,
88
+ onCodeScanned,
89
+ ...viewProps
90
+ },
91
+ ref
92
+ ) {
52
93
  if (Platform.OS !== 'ios' && Platform.OS !== 'android')
53
94
  throw new Error('CameraView requires a native build')
54
95
  if (typeof active !== 'boolean') throw new TypeError('CameraView.active must be a boolean')
@@ -58,16 +99,79 @@ export function CameraView({
58
99
  if (!Array.isArray(codeTypes) || codeTypes.some((type) => !isCodeType(type))) {
59
100
  throw new TypeError('CameraView.codeTypes contains an unsupported code type')
60
101
  }
102
+ if (flash !== 'off' && flash !== 'on' && flash !== 'auto') {
103
+ throw new TypeError('CameraView.flash must be off, on or auto')
104
+ }
105
+ if (typeof enableTorch !== 'boolean') {
106
+ throw new TypeError('CameraView.enableTorch must be a boolean')
107
+ }
108
+ if (typeof zoom !== 'number' || !(zoom >= 0 && zoom <= 1)) {
109
+ throw new RangeError('CameraView.zoom must be a number from 0 to 1')
110
+ }
111
+ const nativeRef = useRef<React.ElementRef<typeof NativeCamera>>(null)
112
+ const pending = useRef(new Map<number, PendingPicture>())
113
+
114
+ useEffect(() => {
115
+ const requests = pending.current
116
+ return () => {
117
+ for (const request of requests.values())
118
+ request.reject(codedError('E_CAMERA_UNMOUNTED', 'CameraView unmounted before the picture was taken'))
119
+ requests.clear()
120
+ }
121
+ }, [])
122
+
123
+ useImperativeHandle(
124
+ ref,
125
+ () => ({
126
+ takePicture(options = {}) {
127
+ const { quality = 1, base64 = false } = options
128
+ if (typeof quality !== 'number' || !(quality >= 0 && quality <= 1))
129
+ return Promise.reject(new RangeError('takePicture quality must be a number from 0 to 1'))
130
+ if (typeof base64 !== 'boolean')
131
+ return Promise.reject(new TypeError('takePicture base64 must be a boolean'))
132
+ const view = nativeRef.current
133
+ if (!view)
134
+ return Promise.reject(codedError('E_CAMERA_UNMOUNTED', 'CameraView is not mounted'))
135
+ const requestId = nextRequestId++
136
+ return new Promise<CameraCapturedPicture>((resolve, reject) => {
137
+ pending.current.set(requestId, { resolve, reject, base64 })
138
+ Commands.takePicture(view, requestId, quality, base64)
139
+ })
140
+ },
141
+ }),
142
+ []
143
+ )
144
+
61
145
  return (
62
146
  <NativeCamera
63
147
  {...viewProps}
148
+ ref={nativeRef}
64
149
  active={active}
65
150
  facing={facing}
66
151
  codeTypes={[...new Set(codeTypes)]}
152
+ flash={flash}
153
+ enableTorch={enableTorch}
154
+ zoom={zoom}
67
155
  onNativeCameraState={({ nativeEvent }) => onStateChange?.(cameraState(nativeEvent.state))}
68
156
  onNativeCameraCode={({ nativeEvent }) =>
69
157
  onCodeScanned?.({ type: codeType(nativeEvent.type), data: nativeEvent.data })
70
158
  }
159
+ onNativeCameraPicture={({ nativeEvent }) => {
160
+ const request = pending.current.get(nativeEvent.requestId)
161
+ if (!request) return
162
+ pending.current.delete(nativeEvent.requestId)
163
+ if (nativeEvent.error) {
164
+ request.reject(nativeError(nativeEvent.error))
165
+ return
166
+ }
167
+ request.resolve({
168
+ uri: nativeEvent.uri,
169
+ width: nativeEvent.width,
170
+ height: nativeEvent.height,
171
+ format: 'jpg',
172
+ ...(request.base64 ? { base64: nativeEvent.base64 } : {}),
173
+ })
174
+ }}
71
175
  />
72
176
  )
73
- }
177
+ })
@@ -1,7 +1,18 @@
1
- import type { CameraViewProps } from './types'
1
+ import { forwardRef } from 'react'
2
+ import type { CameraViewProps, CameraViewRef } from './types'
2
3
 
3
- export type { CameraCode, CameraCodeType, CameraFacing, CameraState, CameraViewProps } from './types'
4
+ export type {
5
+ CameraCapturedPicture,
6
+ CameraCode,
7
+ CameraCodeType,
8
+ CameraFacing,
9
+ CameraFlashMode,
10
+ CameraPictureOptions,
11
+ CameraState,
12
+ CameraViewProps,
13
+ CameraViewRef,
14
+ } from './types'
4
15
 
5
- export function CameraView(_props: CameraViewProps): never {
16
+ export const CameraView = forwardRef<CameraViewRef, CameraViewProps>(function CameraView(): never {
6
17
  throw new Error('CameraView requires a native build')
7
- }
18
+ })
@@ -27,10 +27,32 @@ export interface CameraCode {
27
27
  data: string
28
28
  }
29
29
 
30
+ export type CameraFlashMode = 'off' | 'on' | 'auto'
31
+
30
32
  export interface CameraViewProps extends ViewProps {
31
33
  active?: boolean
32
34
  facing?: CameraFacing
33
35
  codeTypes?: readonly CameraCodeType[]
36
+ flash?: CameraFlashMode
37
+ enableTorch?: boolean
38
+ zoom?: number
34
39
  onStateChange?: (state: CameraState) => void
35
40
  onCodeScanned?: (code: CameraCode) => void
36
41
  }
42
+
43
+ export interface CameraPictureOptions {
44
+ quality?: number
45
+ base64?: boolean
46
+ }
47
+
48
+ export interface CameraCapturedPicture {
49
+ uri: string
50
+ width: number
51
+ height: number
52
+ format: 'jpg'
53
+ base64?: string
54
+ }
55
+
56
+ export interface CameraViewRef {
57
+ takePicture(options?: CameraPictureOptions): Promise<CameraCapturedPicture>
58
+ }
@@ -214,11 +214,15 @@ export type {
214
214
  export { Audio } from './audio/index'
215
215
  export { CameraView } from './camera/index.native'
216
216
  export type {
217
+ CameraCapturedPicture,
217
218
  CameraCode,
218
219
  CameraCodeType,
219
220
  CameraFacing,
221
+ CameraFlashMode,
222
+ CameraPictureOptions,
220
223
  CameraState,
221
224
  CameraViewProps,
225
+ CameraViewRef,
222
226
  } from './camera/index.native'
223
227
  export type {
224
228
  AudioPlaybackState,
@@ -88,11 +88,15 @@ export type {
88
88
  export { Audio } from './audio/index'
89
89
  export { CameraView } from './camera/index'
90
90
  export type {
91
+ CameraCapturedPicture,
91
92
  CameraCode,
92
93
  CameraCodeType,
93
94
  CameraFacing,
95
+ CameraFlashMode,
96
+ CameraPictureOptions,
94
97
  CameraState,
95
98
  CameraViewProps,
99
+ CameraViewRef,
96
100
  } from './camera/index'
97
101
  export type {
98
102
  AudioPlaybackState,
@@ -1,13 +1,46 @@
1
- import type { ViewProps } from 'react-native'
2
- import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes'
1
+ import type { HostComponent, ViewProps } from 'react-native'
2
+ import type {
3
+ DirectEventHandler,
4
+ Double,
5
+ Int32,
6
+ } from 'react-native/Libraries/Types/CodegenTypes'
7
+ import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'
3
8
  import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'
9
+ import type * as React from 'react'
4
10
 
5
11
  interface NativeProps extends ViewProps {
6
12
  active: boolean
7
13
  facing: string
8
14
  codeTypes: ReadonlyArray<string>
15
+ flash: string
16
+ enableTorch: boolean
17
+ zoom: Double
9
18
  onNativeCameraState?: DirectEventHandler<Readonly<{ state: string }>>
10
19
  onNativeCameraCode?: DirectEventHandler<Readonly<{ type: string; data: string }>>
20
+ // error is empty on success, otherwise "<code>: <message>".
21
+ onNativeCameraPicture?: DirectEventHandler<
22
+ Readonly<{
23
+ requestId: Int32
24
+ uri: string
25
+ width: Int32
26
+ height: Int32
27
+ base64: string
28
+ error: string
29
+ }>
30
+ >
11
31
  }
12
32
 
33
+ interface NativeCommands {
34
+ takePicture(
35
+ viewRef: React.ElementRef<HostComponent<NativeProps>>,
36
+ requestId: Int32,
37
+ quality: Double,
38
+ base64: boolean
39
+ ): void
40
+ }
41
+
42
+ export const Commands = codegenNativeCommands<NativeCommands>({
43
+ supportedCommands: ['takePicture'],
44
+ })
45
+
13
46
  export default codegenNativeComponent<NativeProps>('OneNativeCamera')