diff --git a/ReactNativeCameraKit.podspec b/ReactNativeCameraKit.podspec index 5c2565bf5..c7f5bf673 100644 --- a/ReactNativeCameraKit.podspec +++ b/ReactNativeCameraKit.podspec @@ -14,6 +14,21 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/teslamotors/react-native-camera-kit.git", :tag => "v#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,swift}" + s.private_header_files = 'ios/ReactNativeCameraKit/ReactNativeCameraKit-Swift.pre.h' + + if ENV['USE_FRAMEWORKS'] + exisiting_flags = s.attributes_hash["compiler_flags"] + if exisiting_flags.present? + s.compiler_flags = exisiting_flags + "-DCK_USE_FRAMEWORKS=1" + else + s.compiler_flags = "-DCK_USE_FRAMEWORKS=1" + end + end + + if defined?(install_modules_dependencies()) != nil + install_modules_dependencies(s) + else + s.dependency 'React-Core' + end - s.dependency 'React-Core' end diff --git a/android/build.gradle b/android/build.gradle index daa208496..38cc502b2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,23 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' +def isNewArchitectureEnabled() { + // To opt-in for the New Architecture, you can either: + // - Set `newArchEnabled` to true inside the `gradle.properties` file + // - Invoke gradle with `-newArchEnabled=true` + // - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true` + return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true" +} + +if (isNewArchitectureEnabled()) { + apply plugin: 'com.facebook.react' +} + android { def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION if (agpVersion.tokenize('.')[0].toInteger() >= 7) { namespace "com.rncamerakit" } - compileSdkVersion = 34 defaultConfig { minSdkVersion = 24 @@ -17,10 +28,19 @@ android { ndk { abiFilters "arm64-v8a", "x86" } + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() } lintOptions { warning 'InvalidPackage' } + + if (!isNewArchitectureEnabled()) { + sourceSets { + main { + java.srcDirs += 'src/paper/java' + } + } + } } dependencies { diff --git a/android/src/main/java/com/rncamerakit/CKCamera.kt b/android/src/main/java/com/rncamerakit/CKCamera.kt index 987d46cdc..d2a2a9766 100644 --- a/android/src/main/java/com/rncamerakit/CKCamera.kt +++ b/android/src/main/java/com/rncamerakit/CKCamera.kt @@ -41,7 +41,9 @@ import kotlin.math.min import android.graphics.Canvas import android.graphics.Paint import android.graphics.RectF +import com.facebook.react.uimanager.UIManagerHelper import com.google.mlkit.vision.barcode.common.Barcode +import com.rncamerakit.events.* class RectOverlay constructor(context: Context) : View(context) { @@ -330,13 +332,10 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs } catch (exc: Exception) { Log.e(TAG, "Use case binding failed", exc) - val event: WritableMap = Arguments.createMap() - event.putString("errorMessage", exc.message) - currentContext.getJSModule(RCTEventEmitter::class.java).receiveEvent( - id, - "onError", - event - ) + val surfaceId = UIManagerHelper.getSurfaceId(currentContext) + UIManagerHelper + .getEventDispatcherForReactTag(currentContext, id) + ?.dispatchEvent(ErrorEvent(surfaceId, id, exc.message)) } } @@ -459,15 +458,11 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs } private fun onBarcodeRead(barcodes: List) { - val event: WritableMap = Arguments.createMap() - event.putString("codeStringValue", barcodes.first().rawValue) val codeFormat = CodeFormat.fromBarcodeType(barcodes.first().format); - event.putString("codeFormat",codeFormat.code ); - currentContext.getJSModule(RCTEventEmitter::class.java).receiveEvent( - id, - "onReadCode", - event - ) + val surfaceId = UIManagerHelper.getSurfaceId(currentContext) + UIManagerHelper + .getEventDispatcherForReactTag(currentContext, id) + ?.dispatchEvent(ReadCodeEvent(surfaceId, id, barcodes.first().rawValue, codeFormat.code)) } private fun onOrientationChange(orientation: Int) { @@ -482,23 +477,17 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs } } - val event: WritableMap = Arguments.createMap() - event.putInt("orientation", remappedOrientation) - currentContext.getJSModule(RCTEventEmitter::class.java).receiveEvent( - id, - "onOrientationChange", - event - ) + val surfaceId = UIManagerHelper.getSurfaceId(currentContext) + UIManagerHelper + .getEventDispatcherForReactTag(currentContext, id) + ?.dispatchEvent(OrientationChangeEvent(surfaceId, id, remappedOrientation)) } private fun onPictureTaken(uri: String) { - val event: WritableMap = Arguments.createMap() - event.putString("uri", uri) - currentContext.getJSModule(RCTEventEmitter::class.java).receiveEvent( - id, - "onPictureTaken", - event - ) + val surfaceId = UIManagerHelper.getSurfaceId(currentContext) + UIManagerHelper + .getEventDispatcherForReactTag(currentContext, id) + ?.dispatchEvent(PictureTakenEvent(surfaceId, id, uri)) } fun setFlashMode(mode: String?) { @@ -569,13 +558,10 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs } lastOnZoom = desiredOrCameraZoom - val event: WritableMap = Arguments.createMap() - event.putDouble("zoom", desiredOrCameraZoom) - currentContext.getJSModule(RCTEventEmitter::class.java).receiveEvent( - id, - "onZoom", - event - ) + val surfaceId = UIManagerHelper.getSurfaceId(currentContext) + UIManagerHelper + .getEventDispatcherForReactTag(currentContext, id) + ?.dispatchEvent(ZoomEvent(surfaceId, id, desiredOrCameraZoom)) } fun setMaxZoom(factor: Double?) { diff --git a/android/src/main/java/com/rncamerakit/CKCameraManager.kt b/android/src/main/java/com/rncamerakit/CKCameraManager.kt index 9b1cda918..6c3b22acb 100644 --- a/android/src/main/java/com/rncamerakit/CKCameraManager.kt +++ b/android/src/main/java/com/rncamerakit/CKCameraManager.kt @@ -9,13 +9,20 @@ import com.facebook.react.common.MapBuilder import com.facebook.react.common.ReactConstants.TAG import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewManagerDelegate import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.viewmanagers.CKCameraManagerDelegate +import com.facebook.react.viewmanagers.CKCameraManagerInterface +import com.rncamerakit.events.* +class CKCameraManager : SimpleViewManager(), CKCameraManagerInterface { -class CKCameraManager : SimpleViewManager() { + private val delegate: ViewManagerDelegate = CKCameraManagerDelegate(this) + + override fun getDelegate(): ViewManagerDelegate = delegate override fun getName() : String { - return "CKCameraManager" + return "CKCamera" } override fun createViewInstance(context: ThemedReactContext): CKCamera { @@ -44,81 +51,94 @@ class CKCameraManager : SimpleViewManager() { override fun getExportedCustomDirectEventTypeConstants(): Map { return MapBuilder.of( - "onOrientationChange", MapBuilder.of("registrationName", "onOrientationChange"), - "onReadCode", MapBuilder.of("registrationName", "onReadCode"), - "onPictureTaken", MapBuilder.of("registrationName", "onPictureTaken"), - "onZoom", MapBuilder.of("registrationName", "onZoom"), - "onError", MapBuilder.of("registrationName", "onError") + OrientationChangeEvent.EVENT_NAME, MapBuilder.of("registrationName", "onOrientationChange"), + ReadCodeEvent.EVENT_NAME, MapBuilder.of("registrationName", "onReadCode"), + PictureTakenEvent.EVENT_NAME, MapBuilder.of("registrationName", "onPictureTaken"), + ZoomEvent.EVENT_NAME, MapBuilder.of("registrationName", "onZoom"), + ErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onError") ) } @ReactProp(name = "cameraType") - fun setCameraType(view: CKCamera, type: String) { - view.setCameraType(type) + override fun setCameraType(view: CKCamera, type: String?) { + view.setCameraType(type ?: "back") } @ReactProp(name = "flashMode") - fun setFlashMode(view: CKCamera, mode: String?) { + override fun setFlashMode(view: CKCamera, mode: String?) { view.setFlashMode(mode) } @ReactProp(name = "torchMode") - fun setTorchMode(view: CKCamera, mode: String?) { + override fun setTorchMode(view: CKCamera, mode: String?) { view.setTorchMode(mode) } @ReactProp(name = "focusMode") - fun setFocusMode(view: CKCamera, mode: String) { - view.setAutoFocus(mode) + override fun setFocusMode(view: CKCamera, mode: String?) { + view.setAutoFocus(mode ?: "on") } @ReactProp(name = "zoomMode") - fun setZoomMode(view: CKCamera, mode: String?) { + override fun setZoomMode(view: CKCamera, mode: String?) { view.setZoomMode(mode) } @ReactProp(name = "zoom", defaultDouble = -1.0) - fun setZoom(view: CKCamera, factor: Double) { + override fun setZoom(view: CKCamera, factor: Double) { view.setZoom(if (factor == -1.0) null else factor) } @ReactProp(name = "maxZoom", defaultDouble = 420.0) - fun setMaxZoom(view: CKCamera, factor: Double) { + override fun setMaxZoom(view: CKCamera, factor: Double) { view.setMaxZoom(factor) } @ReactProp(name = "scanBarcode") - fun setScanBarcode(view: CKCamera, enabled: Boolean) { + override fun setScanBarcode(view: CKCamera, enabled: Boolean) { view.setScanBarcode(enabled) } @ReactProp(name = "showFrame") - fun setShowFrame(view: CKCamera, enabled: Boolean) { + override fun setShowFrame(view: CKCamera, enabled: Boolean) { view.setShowFrame(enabled) } @ReactProp(name = "laserColor", defaultInt = Color.RED) - fun setLaserColor(view: CKCamera, @ColorInt color: Int) { - view.setLaserColor(color) + override fun setLaserColor(view: CKCamera, @ColorInt color: Int?) { + view.setLaserColor(color ?: Color.RED) } @ReactProp(name = "frameColor", defaultInt = Color.GREEN) - fun setFrameColor(view: CKCamera, @ColorInt color: Int) { - view.setFrameColor(color) + override fun setFrameColor(view: CKCamera, @ColorInt color: Int?) { + view.setFrameColor(color ?: Color.GREEN) } @ReactProp(name = "outputPath") - fun setOutputPath(view: CKCamera, path: String) { - view.setOutputPath(path) + override fun setOutputPath(view: CKCamera, path: String?) { + view.setOutputPath(path ?: "") } @ReactProp(name = "shutterAnimationDuration") - fun setShutterAnimationDuration(view: CKCamera, duration: Int) { + override fun setShutterAnimationDuration(view: CKCamera, duration: Int) { view.setShutterAnimationDuration(duration) } @ReactProp(name = "shutterPhotoSound") - fun setShutterPhotoSound(view: CKCamera, enabled: Boolean) { + override fun setShutterPhotoSound(view: CKCamera, enabled: Boolean) { view.setShutterPhotoSound(enabled); } -} \ No newline at end of file + + // Methods only available on iOS + override fun setRatioOverlay(view: CKCamera?, value: String?) = Unit + + override fun setRatioOverlayColor(view: CKCamera?, value: Int?) = Unit + + override fun setResetFocusTimeout(view: CKCamera?, value: Int) = Unit + + override fun setResetFocusWhenMotionDetected(view: CKCamera?, value: Boolean) = Unit + + override fun setResizeMode(view: CKCamera?, value: String?) = Unit + + override fun setScanThrottleDelay(view: CKCamera?, value: Int) = Unit +} diff --git a/android/src/main/java/com/rncamerakit/RNCameraKitModule.kt b/android/src/main/java/com/rncamerakit/RNCameraKitModule.kt index 2761f2ae2..15d7cf608 100644 --- a/android/src/main/java/com/rncamerakit/RNCameraKitModule.kt +++ b/android/src/main/java/com/rncamerakit/RNCameraKitModule.kt @@ -1,7 +1,7 @@ package com.rncamerakit import com.facebook.react.bridge.* -import com.facebook.react.uimanager.UIManagerModule +import com.facebook.react.uimanager.UIManagerHelper /** * Native module for interacting with the camera in React Native applications. @@ -11,7 +11,7 @@ import com.facebook.react.uimanager.UIManagerModule * * @param reactContext The application's ReactApplicationContext. */ -class RNCameraKitModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { +class RNCameraKitModule(private val reactContext: ReactApplicationContext) : NativeCameraKitModuleSpec(reactContext) { companion object { // Constants for camera orientation @@ -34,10 +34,12 @@ class RNCameraKitModule(private val reactContext: ReactApplicationContext) : Rea * Represents the landscape orientation with the right side of the device up. */ const val LANDSCAPE_RIGHT = 3 // ➡️ + + const val REACT_CLASS = "RNCameraKitModule" } override fun getName(): String { - return "RNCameraKitModule" + return REACT_CLASS } /** @@ -54,6 +56,10 @@ class RNCameraKitModule(private val reactContext: ReactApplicationContext) : Rea ) } + override fun requestDeviceCameraAuthorization(promise: Promise?) = Unit + + override fun checkDeviceCameraAuthorizationStatus(promise: Promise?) = Unit + /** * Captures a photo using the camera. * @@ -62,13 +68,16 @@ class RNCameraKitModule(private val reactContext: ReactApplicationContext) : Rea * @param promise The promise to resolve the capture result. */ @ReactMethod - fun capture(options: ReadableMap, viewTag: Int, promise: Promise) { - // CameraManager does not allow us to return values - val context = reactContext - val uiManager = context.getNativeModule(UIManagerModule::class.java) - context.runOnUiQueueThread { - val view = uiManager?.resolveView(viewTag) as CKCamera - view.capture(options.toHashMap(), promise) + override fun capture(options: ReadableMap?, tag: Double?, promise: Promise) { + val viewTag = tag?.toInt() + if (viewTag != null && options != null) { + val uiManager = UIManagerHelper.getUIManagerForReactTag(reactContext, viewTag) + reactContext.runOnUiQueueThread { + val camera = uiManager?.resolveView(viewTag) as CKCamera + camera.capture(options.toHashMap(), promise) + } + } else { + promise.reject("E_CAPTURE_FAILED", "options or/and tag arguments are null, options: $options, tag: $viewTag") } } } diff --git a/android/src/main/java/com/rncamerakit/RNCameraKitPackage.kt b/android/src/main/java/com/rncamerakit/RNCameraKitPackage.kt index fc14225bc..02cb26e05 100644 --- a/android/src/main/java/com/rncamerakit/RNCameraKitPackage.kt +++ b/android/src/main/java/com/rncamerakit/RNCameraKitPackage.kt @@ -1,22 +1,43 @@ package com.rncamerakit -import com.facebook.react.ReactPackage +import com.facebook.react.TurboReactPackage import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.annotations.ReactModuleList +import com.facebook.react.module.model.ReactModuleInfo +import com.facebook.react.module.model.ReactModuleInfoProvider import com.facebook.react.uimanager.ViewManager import java.util.* -class RNCameraKitPackage : ReactPackage { - override fun createNativeModules(reactContext: ReactApplicationContext): List { - val modules: MutableList = ArrayList() - val cameraModule = RNCameraKitModule(reactContext) - modules.add(cameraModule) - return modules - } +@ReactModuleList(nativeModules = [RNCameraKitModule::class]) +class RNCameraKitPackage : TurboReactPackage() { override fun createViewManagers(reactContext: ReactApplicationContext): List> { val viewManagers: MutableList> = ArrayList() viewManagers.add(CKCameraManager()) return viewManagers } -} \ No newline at end of file + + override fun getModule(s: String, reactApplicationContext: ReactApplicationContext): NativeModule? { + when (s) { + RNCameraKitModule.REACT_CLASS -> return RNCameraKitModule(reactApplicationContext) + } + return null + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { + val moduleInfos: MutableMap = HashMap() + val isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + moduleInfos[RNCameraKitModule.REACT_CLASS] = ReactModuleInfo( + RNCameraKitModule.REACT_CLASS, + RNCameraKitModule.REACT_CLASS, + false, // canOverrideExistingModule + false, // needsEagerInit + false, // isCxxModule + isTurboModule // isTurboModule + ) + moduleInfos + } + } +} diff --git a/android/src/main/java/com/rncamerakit/events/ErrorEvent.kt b/android/src/main/java/com/rncamerakit/events/ErrorEvent.kt new file mode 100644 index 000000000..a01542595 --- /dev/null +++ b/android/src/main/java/com/rncamerakit/events/ErrorEvent.kt @@ -0,0 +1,22 @@ +package com.rncamerakit.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event + +class ErrorEvent( + surfaceId: Int, + viewId: Int, + private val errorMessage: String?, +) : Event(surfaceId, viewId) { + override fun getEventName(): String = EVENT_NAME + + override fun getEventData(): WritableMap = + Arguments.createMap().apply { + putString("errorMessage", errorMessage) + } + + companion object { + const val EVENT_NAME = "topError" + } +} diff --git a/android/src/main/java/com/rncamerakit/events/OrientationChangeEvent.kt b/android/src/main/java/com/rncamerakit/events/OrientationChangeEvent.kt new file mode 100644 index 000000000..5685f4ffa --- /dev/null +++ b/android/src/main/java/com/rncamerakit/events/OrientationChangeEvent.kt @@ -0,0 +1,22 @@ +package com.rncamerakit.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event + +class OrientationChangeEvent( + surfaceId: Int, + viewId: Int, + private val orientation: Int, +) : Event(surfaceId, viewId) { + override fun getEventName(): String = EVENT_NAME + + override fun getEventData(): WritableMap = + Arguments.createMap().apply { + putInt("orientation", orientation) + } + + companion object { + const val EVENT_NAME = "topOrientationChange" + } +} diff --git a/android/src/main/java/com/rncamerakit/events/PictureTakenEvent.kt b/android/src/main/java/com/rncamerakit/events/PictureTakenEvent.kt new file mode 100644 index 000000000..fc71ba740 --- /dev/null +++ b/android/src/main/java/com/rncamerakit/events/PictureTakenEvent.kt @@ -0,0 +1,22 @@ +package com.rncamerakit.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event + +class PictureTakenEvent( + surfaceId: Int, + viewId: Int, + private val uri: String, +) : Event(surfaceId, viewId) { + override fun getEventName(): String = EVENT_NAME + + override fun getEventData(): WritableMap = + Arguments.createMap().apply { + putString("uri", uri) + } + + companion object { + const val EVENT_NAME = "topPictureTaken" + } +} diff --git a/android/src/main/java/com/rncamerakit/events/ReadCodeEvent.kt b/android/src/main/java/com/rncamerakit/events/ReadCodeEvent.kt new file mode 100644 index 000000000..ff3d3a746 --- /dev/null +++ b/android/src/main/java/com/rncamerakit/events/ReadCodeEvent.kt @@ -0,0 +1,24 @@ +package com.rncamerakit.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event + +class ReadCodeEvent( + surfaceId: Int, + viewId: Int, + private val codeStringValue: String?, + private val codeFormat: String, +) : Event(surfaceId, viewId) { + override fun getEventName(): String = EVENT_NAME + + override fun getEventData(): WritableMap = + Arguments.createMap().apply { + putString("codeFormat", codeFormat) + putString("codeStringValue", codeStringValue) + } + + companion object { + const val EVENT_NAME = "topReadCode" + } +} diff --git a/android/src/main/java/com/rncamerakit/events/ZoomEvent.kt b/android/src/main/java/com/rncamerakit/events/ZoomEvent.kt new file mode 100644 index 000000000..55626827a --- /dev/null +++ b/android/src/main/java/com/rncamerakit/events/ZoomEvent.kt @@ -0,0 +1,22 @@ +package com.rncamerakit.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event + +class ZoomEvent( + surfaceId: Int, + viewId: Int, + private val zoom: Double, +) : Event(surfaceId, viewId) { + override fun getEventName(): String = EVENT_NAME + + override fun getEventData(): WritableMap = + Arguments.createMap().apply { + putDouble("zoom", zoom) + } + + companion object { + const val EVENT_NAME = "topZoom" + } +} diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/CKCameraManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/CKCameraManagerDelegate.java new file mode 100644 index 000000000..96c9b840e --- /dev/null +++ b/android/src/paper/java/com/facebook/react/viewmanagers/CKCameraManagerDelegate.java @@ -0,0 +1,89 @@ +/** +* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). +* +* Do not edit this file as changes may cause incorrect behavior and will be lost +* once the code is regenerated. +* +* @generated by codegen project: GeneratePropsJavaDelegate.js +*/ + +package com.facebook.react.viewmanagers; + +import android.view.View; +import androidx.annotation.Nullable; +import com.facebook.react.bridge.ColorPropConverter; +import com.facebook.react.uimanager.BaseViewManagerDelegate; +import com.facebook.react.uimanager.BaseViewManagerInterface; + +public class CKCameraManagerDelegate & CKCameraManagerInterface> extends BaseViewManagerDelegate { + public CKCameraManagerDelegate(U viewManager) { + super(viewManager); + } + @Override + public void setProperty(T view, String propName, @Nullable Object value) { + switch (propName) { + case "flashMode": + mViewManager.setFlashMode(view, value == null ? null : (String) value); + break; + case "focusMode": + mViewManager.setFocusMode(view, value == null ? null : (String) value); + break; + case "zoomMode": + mViewManager.setZoomMode(view, value == null ? null : (String) value); + break; + case "zoom": + mViewManager.setZoom(view, value == null ? 0f : ((Double) value).doubleValue()); + break; + case "maxZoom": + mViewManager.setMaxZoom(view, value == null ? 0f : ((Double) value).doubleValue()); + break; + case "torchMode": + mViewManager.setTorchMode(view, value == null ? null : (String) value); + break; + case "cameraType": + mViewManager.setCameraType(view, value == null ? null : (String) value); + break; + case "scanBarcode": + mViewManager.setScanBarcode(view, value == null ? false : (boolean) value); + break; + case "showFrame": + mViewManager.setShowFrame(view, value == null ? false : (boolean) value); + break; + case "laserColor": + mViewManager.setLaserColor(view, ColorPropConverter.getColor(value, view.getContext())); + break; + case "frameColor": + mViewManager.setFrameColor(view, ColorPropConverter.getColor(value, view.getContext())); + break; + case "ratioOverlay": + mViewManager.setRatioOverlay(view, value == null ? null : (String) value); + break; + case "ratioOverlayColor": + mViewManager.setRatioOverlayColor(view, ColorPropConverter.getColor(value, view.getContext())); + break; + case "resetFocusTimeout": + mViewManager.setResetFocusTimeout(view, value == null ? 0 : ((Double) value).intValue()); + break; + case "resetFocusWhenMotionDetected": + mViewManager.setResetFocusWhenMotionDetected(view, value == null ? false : (boolean) value); + break; + case "resizeMode": + mViewManager.setResizeMode(view, value == null ? null : (String) value); + break; + case "scanThrottleDelay": + mViewManager.setScanThrottleDelay(view, value == null ? 0 : ((Double) value).intValue()); + break; + case "shutterPhotoSound": + mViewManager.setShutterPhotoSound(view, value == null ? false : (boolean) value); + break; + case "shutterAnimationDuration": + mViewManager.setShutterAnimationDuration(view, value == null ? 0 : ((Double) value).intValue()); + break; + case "outputPath": + mViewManager.setOutputPath(view, value == null ? null : (String) value); + break; + default: + super.setProperty(view, propName, value); + } + } +} diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/CKCameraManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/CKCameraManagerInterface.java new file mode 100644 index 000000000..25cd980c3 --- /dev/null +++ b/android/src/paper/java/com/facebook/react/viewmanagers/CKCameraManagerInterface.java @@ -0,0 +1,36 @@ +/** +* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). +* +* Do not edit this file as changes may cause incorrect behavior and will be lost +* once the code is regenerated. +* +* @generated by codegen project: GeneratePropsJavaInterface.js +*/ + +package com.facebook.react.viewmanagers; + +import android.view.View; +import androidx.annotation.Nullable; + +public interface CKCameraManagerInterface { + void setFlashMode(T view, @Nullable String value); + void setFocusMode(T view, @Nullable String value); + void setZoomMode(T view, @Nullable String value); + void setZoom(T view, double value); + void setMaxZoom(T view, double value); + void setTorchMode(T view, @Nullable String value); + void setCameraType(T view, @Nullable String value); + void setScanBarcode(T view, boolean value); + void setShowFrame(T view, boolean value); + void setLaserColor(T view, @Nullable Integer value); + void setFrameColor(T view, @Nullable Integer value); + void setRatioOverlay(T view, @Nullable String value); + void setRatioOverlayColor(T view, @Nullable Integer value); + void setResetFocusTimeout(T view, int value); + void setResetFocusWhenMotionDetected(T view, boolean value); + void setResizeMode(T view, @Nullable String value); + void setScanThrottleDelay(T view, int value); + void setShutterPhotoSound(T view, boolean value); + void setShutterAnimationDuration(T view, int value); + void setOutputPath(T view, @Nullable String value); +} diff --git a/android/src/paper/java/com/rncamerakit/NativeCameraKitModuleSpec.java b/android/src/paper/java/com/rncamerakit/NativeCameraKitModuleSpec.java new file mode 100644 index 000000000..7f1a03e8c --- /dev/null +++ b/android/src/paper/java/com/rncamerakit/NativeCameraKitModuleSpec.java @@ -0,0 +1,48 @@ + +/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateModuleJavaSpec.js + * + * @nolint + */ + +package com.rncamerakit; + +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public abstract class NativeCameraKitModuleSpec extends ReactContextBaseJavaModule implements TurboModule { + public static final String NAME = "RNCameraKitModule"; + + public NativeCameraKitModuleSpec(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public @Nonnull String getName() { + return NAME; + } + + @ReactMethod + @DoNotStrip + public abstract void capture(@Nullable ReadableMap options, @Nullable Double tag, Promise promise); + + @ReactMethod + @DoNotStrip + public abstract void requestDeviceCameraAuthorization(Promise promise); + + @ReactMethod + @DoNotStrip + public abstract void checkDeviceCameraAuthorizationStatus(Promise promise); +} diff --git a/ios/ReactNativeCameraKit/CKCameraManager.m b/ios/ReactNativeCameraKit/CKCameraManager.m index 82f39b1d0..72ac82f3e 100644 --- a/ios/ReactNativeCameraKit/CKCameraManager.m +++ b/ios/ReactNativeCameraKit/CKCameraManager.m @@ -41,14 +41,4 @@ @interface RCT_EXTERN_MODULE(CKCameraManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(zoom, NSNumber) RCT_EXPORT_VIEW_PROPERTY(maxZoom, NSNumber) -RCT_EXTERN_METHOD(capture:(NSDictionary*)options - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(checkDeviceCameraAuthorizationStatus:(RCTPromiseResolveBlock)resolve - reject:(__unused RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(requestDeviceCameraAuthorization:(RCTPromiseResolveBlock)resolve - reject:(__unused RCTPromiseRejectBlock)reject) - @end diff --git a/ios/ReactNativeCameraKit/CKCameraViewComponentView.h b/ios/ReactNativeCameraKit/CKCameraViewComponentView.h new file mode 100644 index 000000000..28ade05a9 --- /dev/null +++ b/ios/ReactNativeCameraKit/CKCameraViewComponentView.h @@ -0,0 +1,15 @@ +#ifdef RCT_NEW_ARCH_ENABLED + +#import + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface CKCameraViewComponentView : RCTViewComponentView +@end + +NS_ASSUME_NONNULL_END + +#endif // RCT_NEW_ARCH_ENABLED diff --git a/ios/ReactNativeCameraKit/CKCameraViewComponentView.mm b/ios/ReactNativeCameraKit/CKCameraViewComponentView.mm new file mode 100644 index 000000000..5924c8243 --- /dev/null +++ b/ios/ReactNativeCameraKit/CKCameraViewComponentView.mm @@ -0,0 +1,256 @@ +#ifdef RCT_NEW_ARCH_ENABLED + +#import "CKCameraViewComponentView.h" + +#import +#import +#import +#import + +#import +#import +#import +#import + +#import "ReactNativeCameraKit-Swift.pre.h" + +using namespace facebook::react; + +static id CKConvertFollyDynamicToId(const folly::dynamic &dyn) +{ + // I could imagine an implementation which avoids copies by wrapping the + // dynamic in a derived class of NSDictionary. We can do that if profiling + // implies it will help. + + switch (dyn.type()) { + case folly::dynamic::NULLT: + return nil; + case folly::dynamic::BOOL: + return dyn.getBool() ? @YES : @NO; + case folly::dynamic::INT64: + return @(dyn.getInt()); + case folly::dynamic::DOUBLE: + return @(dyn.getDouble()); + case folly::dynamic::STRING: + return [[NSString alloc] initWithBytes:dyn.c_str() length:dyn.size() encoding:NSUTF8StringEncoding]; + case folly::dynamic::ARRAY: { + NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:dyn.size()]; + for (const auto &elem : dyn) { + id value = CKConvertFollyDynamicToId(elem); + if (value) { + [array addObject:value]; + } + } + return array; + } + case folly::dynamic::OBJECT: { + NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:dyn.size()]; + for (const auto &elem : dyn.items()) { + id key = CKConvertFollyDynamicToId(elem.first); + id value = CKConvertFollyDynamicToId(elem.second); + if (key && value) { + dict[key] = value; + } + } + return dict; + } + } +} + +@interface CKCameraViewComponentView () +@end + +@implementation CKCameraViewComponentView { + CKCameraView *_view; +} + +// Needed because of this: https://github.com/facebook/react-native/pull/37274 ++ (void)load +{ + [super load]; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + [self prepareView]; + } + + return self; +} + +- (void)prepareView +{ + _view = [[CKCameraView alloc] init]; + + // just need to pass something, it won't really be used on fabric, but it's used to create events (it won't impact sending them) + _view.reactTag = @-1; + + __weak __typeof__(self) weakSelf = self; + + [_view setOnReadCode:^(NSDictionary* event) { + __typeof__(self) strongSelf = weakSelf; + + if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) { + std::string codeStringValue = [event valueForKey:@"codeStringValue"] == nil ? "" : std::string([[event valueForKey:@"codeStringValue"] UTF8String]); + std::string codeFormat = [event valueForKey:@"codeFormat"] == nil ? "" : std::string([[event valueForKey:@"codeFormat"] UTF8String]); + std::dynamic_pointer_cast(strongSelf->_eventEmitter)->onReadCode({.codeStringValue = codeStringValue, .codeFormat = codeFormat}); + } + }]; + [_view setOnOrientationChange:^(NSDictionary* event) { + __typeof__(self) strongSelf = weakSelf; + + if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) { + id orientation = [event valueForKey:@"orientation"] == nil ? 0 : [event valueForKey:@"orientation"]; + std::dynamic_pointer_cast(strongSelf->_eventEmitter)->onOrientationChange({.orientation = [orientation intValue]}); + } + }]; + [_view setOnZoom:^(NSDictionary* event) { + __typeof__(self) strongSelf = weakSelf; + + if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) { + id zoom = [event valueForKey:@"zoom"] == nil ? 0 : [event valueForKey:@"zoom"]; + std::dynamic_pointer_cast(strongSelf->_eventEmitter)->onZoom({.zoom = [zoom doubleValue]}); + } + }]; + [_view setOnCaptureButtonPressIn:^(NSDictionary* event) { + __typeof__(self) strongSelf = weakSelf; + + if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) { + std::dynamic_pointer_cast(strongSelf->_eventEmitter)->onCaptureButtonPressIn({}); + } + }]; + [_view setOnCaptureButtonPressOut:^(NSDictionary* event) { + __typeof__(self) strongSelf = weakSelf; + + if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) { + std::dynamic_pointer_cast(strongSelf->_eventEmitter)->onCaptureButtonPressOut({}); + } + }]; + + self.contentView = _view; +} + +#pragma mark - RCTComponentViewProtocol + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +- (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics +{ + [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics]; + [_view updateSubviewsBounds:RCTCGRectFromRect(layoutMetrics.frame)]; +} + +- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps +{ + const auto &newProps = static_cast(*props); + NSMutableArray *changedProps = [NSMutableArray new]; + id cameraType = CKConvertFollyDynamicToId(newProps.cameraType); + if (cameraType != nil && [cameraType isKindOfClass:NSString.class]) { + _view.cameraType = [cameraType isEqualToString:@"back"] ? CKCameraTypeBack : CKCameraTypeFront; + [changedProps addObject:@"cameraType"]; + } + id resizeMode = CKConvertFollyDynamicToId(newProps.resizeMode); + if (resizeMode != nil && [resizeMode isKindOfClass:NSString.class]) { + _view.resizeMode = [resizeMode isEqualToString:@"contain"] ? CKResizeModeContain : CKResizeModeCover; + [changedProps addObject:@"resizeMode"]; + } + id flashMode = CKConvertFollyDynamicToId(newProps.flashMode); + if (flashMode != nil && [flashMode isKindOfClass:NSString.class]) { + _view.flashMode = [flashMode isEqualToString:@"auto"] ? CKFlashModeAuto : [flashMode isEqualToString:@"on"] ? CKFlashModeOn : CKFlashModeOff; + [changedProps addObject:@"flashMode"]; + } + id torchMode = CKConvertFollyDynamicToId(newProps.torchMode); + if (torchMode != nil && [torchMode isKindOfClass:NSString.class]) { + _view.torchMode = [torchMode isEqualToString:@"on"] ? CKTorchModeOn : CKTorchModeOff; + [changedProps addObject:@"torchMode"]; + } + id ratioOverlay = CKConvertFollyDynamicToId(newProps.ratioOverlay); + if (ratioOverlay != nil) { + _view.ratioOverlay = ratioOverlay; + [changedProps addObject:@"ratioOverlay"]; + } + UIColor *ratioOverlayColor = RCTUIColorFromSharedColor(newProps.ratioOverlayColor); + if (ratioOverlayColor != nil) { + _view.ratioOverlayColor = ratioOverlayColor; + [changedProps addObject:@"ratioOverlayColor"]; + } + id scanBarcode = CKConvertFollyDynamicToId(newProps.scanBarcode); + if (scanBarcode != nil) { + _view.scanBarcode = scanBarcode; + [changedProps addObject:@"scanBarcode"]; + } + id showFrame = CKConvertFollyDynamicToId(newProps.showFrame); + if (showFrame != nil) { + _view.showFrame = showFrame; + [changedProps addObject:@"showFrame"]; + } + id scanThrottleDelay = CKConvertFollyDynamicToId(newProps.scanThrottleDelay); + if (scanThrottleDelay != nil) { + _view.scanThrottleDelay = [scanThrottleDelay intValue]; + [changedProps addObject:@"scanThrottleDelay"]; + } + UIColor *frameColor = RCTUIColorFromSharedColor(newProps.frameColor); + if (frameColor != nil) { + _view.frameColor = frameColor; + [changedProps addObject:@"frameColor"]; + } + UIColor *laserColor = RCTUIColorFromSharedColor(newProps.laserColor); + if (laserColor != nil) { + _view.laserColor = laserColor; + [changedProps addObject:@"laserColor"]; + } + id resetFocusTimeout = CKConvertFollyDynamicToId(newProps.resetFocusTimeout); + if (resetFocusTimeout != nil) { + _view.resetFocusTimeout = [resetFocusTimeout intValue]; + [changedProps addObject:@"resetFocusTimeout"]; + } + id resetFocusWhenMotionDetected = CKConvertFollyDynamicToId(newProps.resetFocusWhenMotionDetected); + if (resetFocusWhenMotionDetected != nil) { + _view.resetFocusWhenMotionDetected = resetFocusWhenMotionDetected; + [changedProps addObject:@"resetFocusWhenMotionDetected"]; + } + id focusMode = CKConvertFollyDynamicToId(newProps.focusMode); + if (focusMode != nil) { + _view.focusMode = [focusMode isEqualToString:@"on"] ? CKFocusModeOn : CKFocusModeOff; + [changedProps addObject:@"focusMode"]; + } + id zoomMode = CKConvertFollyDynamicToId(newProps.zoomMode); + if (zoomMode != nil) { + _view.zoomMode = [focusMode isEqualToString:@"on"] ? CKZoomModeOn : CKZoomModeOff; + [changedProps addObject:@"zoomMode"]; + } + id zoom = CKConvertFollyDynamicToId(newProps.zoom); + if (zoom != nil) { + _view.zoom = zoom; + [changedProps addObject:@"zoom"]; + } + id maxZoom = CKConvertFollyDynamicToId(newProps.maxZoom); + if (maxZoom != nil) { + _view.maxZoom = maxZoom; + [changedProps addObject:@"maxZoom"]; + } + + [super updateProps:props oldProps:oldProps]; + [_view didSetProps:changedProps]; +} + +- (void)prepareForRecycle +{ + [super prepareForRecycle]; + [self prepareView]; +} + +@end + +Class CKCameraCls(void) +{ + return CKCameraViewComponentView.class; +} + +#endif // RCT_NEW_ARCH_ENABLED diff --git a/ios/ReactNativeCameraKit/CKTypes+RCTConvert.m b/ios/ReactNativeCameraKit/CKTypes+RCTConvert.m index 47e4e58f3..02b260354 100644 --- a/ios/ReactNativeCameraKit/CKTypes+RCTConvert.m +++ b/ios/ReactNativeCameraKit/CKTypes+RCTConvert.m @@ -11,7 +11,7 @@ #import "RCTConvert.h" #endif -#import "ReactNativeCameraKit-Swift.h" +#import "ReactNativeCameraKit-Swift.pre.h" @implementation RCTConvert (CKTypes) diff --git a/ios/ReactNativeCameraKit/CameraManager.swift b/ios/ReactNativeCameraKit/CameraManager.swift index f105d49b2..98e5fb4d7 100644 --- a/ios/ReactNativeCameraKit/CameraManager.swift +++ b/ios/ReactNativeCameraKit/CameraManager.swift @@ -10,29 +10,23 @@ import Foundation * Class managing the communication between React Native and the native implementation */ @objc(CKCameraManager) public class CameraManager: RCTViewManager { - var camera: CameraView? - override public static func requiresMainQueueSetup() -> Bool { return true } override public func view() -> UIView! { - camera = CameraView() - return camera + return CameraView() } - @objc func capture(_ options: NSDictionary, + @objc public static func capture(camera: CameraView, + options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - guard let cam = self.camera else { - reject("capture_error", "CKCamera capture() was called but camera view is nil", nil) - return - } - cam.capture(onSuccess: { resolve($0) }, + camera.capture(onSuccess: { resolve($0) }, onError: { reject("capture_error", $0, nil) }) } - @objc func checkDeviceCameraAuthorizationStatus(_ resolve: @escaping RCTPromiseResolveBlock, + @objc public static func checkDeviceCameraAuthorizationStatus(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: resolve(true) @@ -41,7 +35,7 @@ import Foundation } } - @objc func requestDeviceCameraAuthorization(_ resolve: @escaping RCTPromiseResolveBlock, + @objc public static func requestDeviceCameraAuthorization(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { AVCaptureDevice.requestAccess(for: .video, completionHandler: { resolve($0) }) } diff --git a/ios/ReactNativeCameraKit/CameraView.swift b/ios/ReactNativeCameraKit/CameraView.swift index cab80ea05..cb3a6f859 100644 --- a/ios/ReactNativeCameraKit/CameraView.swift +++ b/ios/ReactNativeCameraKit/CameraView.swift @@ -12,7 +12,7 @@ import AVKit * Like permission, ratio overlay, focus, zoom gesture, write image, etc */ @objc(CKCameraView) -class CameraView: UIView { +public class CameraView: UIView { private let camera: CameraProtocol // Focus @@ -33,33 +33,33 @@ class CameraView: UIView { // props // camera settings - @objc var cameraType: CameraType = .back - @objc var resizeMode: ResizeMode = .contain - @objc var flashMode: FlashMode = .auto - @objc var torchMode: TorchMode = .off - @objc var maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization = .balanced + @objc public var cameraType: CameraType = .back + @objc public var resizeMode: ResizeMode = .contain + @objc public var flashMode: FlashMode = .auto + @objc public var torchMode: TorchMode = .off + @objc public var maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization = .balanced // ratio overlay - @objc var ratioOverlay: String? - @objc var ratioOverlayColor: UIColor? + @objc public var ratioOverlay: String? + @objc public var ratioOverlayColor: UIColor? // scanner - @objc var scanBarcode = false - @objc var showFrame = false - @objc var onReadCode: RCTDirectEventBlock? - @objc var scanThrottleDelay = 2000 - @objc var frameColor: UIColor? - @objc var laserColor: UIColor? + @objc public var scanBarcode = false + @objc public var showFrame = false + @objc public var onReadCode: RCTDirectEventBlock? + @objc public var scanThrottleDelay = 2000 + @objc public var frameColor: UIColor? + @objc public var laserColor: UIColor? // other - @objc var onOrientationChange: RCTDirectEventBlock? - @objc var onZoom: RCTDirectEventBlock? - @objc var resetFocusTimeout = 0 - @objc var resetFocusWhenMotionDetected = false - @objc var focusMode: FocusMode = .on - @objc var zoomMode: ZoomMode = .on - @objc var zoom: NSNumber? - @objc var maxZoom: NSNumber? - - @objc var onCaptureButtonPressIn: RCTDirectEventBlock? - @objc var onCaptureButtonPressOut: RCTDirectEventBlock? + @objc public var onOrientationChange: RCTDirectEventBlock? + @objc public var onZoom: RCTDirectEventBlock? + @objc public var resetFocusTimeout = 0 + @objc public var resetFocusWhenMotionDetected = false + @objc public var focusMode: FocusMode = .on + @objc public var zoomMode: ZoomMode = .on + @objc public var zoom: NSNumber? + @objc public var maxZoom: NSNumber? + + @objc public var onCaptureButtonPressIn: RCTDirectEventBlock? + @objc public var onCaptureButtonPressOut: RCTDirectEventBlock? var eventInteraction: Any? = nil @@ -139,10 +139,8 @@ class CameraView: UIView { eventInteraction = interaction } } - - - override func removeFromSuperview() { + override public func removeFromSuperview() { camera.cameraRemovedFromSuperview() super.removeFromSuperview() @@ -150,9 +148,12 @@ class CameraView: UIView { // MARK: React lifecycle - override func reactSetFrame(_ frame: CGRect) { + override public func reactSetFrame(_ frame: CGRect) { super.reactSetFrame(frame) - + self.updateSubviewsBounds(frame) + } + + @objc public func updateSubviewsBounds(_ frame: CGRect) { camera.previewView.frame = bounds scannerInterfaceView.frame = bounds @@ -164,14 +165,14 @@ class CameraView: UIView { ratioOverlayView?.frame = bounds } - override func removeReactSubview(_ subview: UIView) { + override public func removeReactSubview(_ subview: UIView) { subview.removeFromSuperview() super.removeReactSubview(subview) } // Called once when all props have been set, then every time one is updated // swiftlint:disable:next cyclomatic_complexity function_body_length - override func didSetProps(_ changedProps: [String]) { + override public func didSetProps(_ changedProps: [String]) { hasPropBeenSetup = true // Camera settings @@ -272,7 +273,7 @@ class CameraView: UIView { // MARK: Public - func capture(onSuccess: @escaping (_ imageObject: [String: Any]) -> Void, + @objc public func capture(onSuccess: @escaping (_ imageObject: [String: Any]) -> Void, onError: @escaping (_ error: String) -> Void) { camera.capturePicture(onWillCapture: { [weak self] in // Flash/dim preview to indicate shutter action diff --git a/ios/ReactNativeCameraKit/RNCameraKitModule.h b/ios/ReactNativeCameraKit/RNCameraKitModule.h new file mode 100644 index 000000000..c5ae1855e --- /dev/null +++ b/ios/ReactNativeCameraKit/RNCameraKitModule.h @@ -0,0 +1,17 @@ +#import +#import + +#ifdef RCT_NEW_ARCH_ENABLED +#import "rncamerakit_specs/rncamerakit_specs.h" +#else +#import +#endif + +@interface RNCameraKitModule : NSObject +#ifdef RCT_NEW_ARCH_ENABLED + +#else + +#endif + +@end diff --git a/ios/ReactNativeCameraKit/RNCameraKitModule.mm b/ios/ReactNativeCameraKit/RNCameraKitModule.mm new file mode 100644 index 000000000..df5e1a024 --- /dev/null +++ b/ios/ReactNativeCameraKit/RNCameraKitModule.mm @@ -0,0 +1,70 @@ +#import +#import +#import +#import "RNCameraKitModule.h" +#ifdef RCT_NEW_ARCH_ENABLED +#import "CKCameraViewComponentView.h" +#endif // RCT_NEW_ARCH_ENABLED + +#import "ReactNativeCameraKit-Swift.pre.h" + +@implementation RNCameraKitModule + +RCT_EXPORT_MODULE(); + +#ifdef RCT_NEW_ARCH_ENABLED +@synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED; +#endif // RCT_NEW_ARCH_ENABLED +@synthesize bridge = _bridge; + +- (dispatch_queue_t)methodQueue +{ + // It seems that due to how UIBlocks work with uiManager, we need to call the methods there + // for the blocks to be dispatched before the batch is completed + return RCTGetUIManagerQueue(); +} + +- (void)withCamera:(nonnull NSNumber*)viewRef block:(void (^)(CKCameraView *))block reject:(RCTPromiseRejectBlock)reject methodName:(NSString *)methodName +{ +#ifdef RCT_NEW_ARCH_ENABLED + [self.viewRegistry_DEPRECATED addUIBlock:^(RCTViewRegistry *viewRegistry) { + CKCameraViewComponentView *componentView = [self.viewRegistry_DEPRECATED viewForReactTag:viewRef]; + CKCameraView *view = componentView.contentView; + +#else + [self.bridge.uiManager + addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + CKCameraView *view = [uiManager viewForReactTag:viewRef]; +#endif // RCT_NEW_ARCH_ENABLED + if (view != nil) { + block(view); + } else { + reject(methodName, [NSString stringWithFormat:@"Unknown reactTag: %@ in %@", viewRef, methodName], nil); + } + }]; +} + +RCT_EXPORT_METHOD(capture:(NSDictionary *)options tag:(nonnull NSNumber *)tag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { + [self withCamera:tag block:^(CKCameraView *view) { + [CKCameraManager captureWithCamera:view options:[NSDictionary new] resolve:resolve reject:reject]; + } reject:reject methodName:@"capture"]; +} + + - (void)checkDeviceCameraAuthorizationStatus:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [CKCameraManager checkDeviceCameraAuthorizationStatus:resolve reject:reject]; +} + + - (void)requestDeviceCameraAuthorization:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [CKCameraManager requestDeviceCameraAuthorization:resolve reject:reject]; +} + + // Thanks to this guard, we won't compile this code when we build for the old architecture. +#ifdef RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} +#endif // RCT_NEW_ARCH_ENABLED + +@end diff --git a/ios/ReactNativeCameraKit/ReactNativeCameraKit-Swift.pre.h b/ios/ReactNativeCameraKit/ReactNativeCameraKit-Swift.pre.h new file mode 100644 index 000000000..f48ce6b11 --- /dev/null +++ b/ios/ReactNativeCameraKit/ReactNativeCameraKit-Swift.pre.h @@ -0,0 +1,5 @@ +#if CK_USE_FRAMEWORKS +#import +#else +#import +#endif diff --git a/package.json b/package.json index 1daf8c40e..a022ee52a 100644 --- a/package.json +++ b/package.json @@ -57,5 +57,13 @@ "engines": { "node": ">=18" }, + "codegenConfig": { + "name": "rncamerakit_specs", + "type": "all", + "jsSrcsDir": "src/specs", + "android": { + "javaPackageName": "com.rncamerakit" + } + }, "packageManager": "yarn@3.6.4" } diff --git a/src/Camera.android.tsx b/src/Camera.android.tsx index ee282a22f..55fc9c247 100644 --- a/src/Camera.android.tsx +++ b/src/Camera.android.tsx @@ -1,19 +1,16 @@ import React from 'react'; -import { requireNativeComponent, findNodeHandle, NativeModules, processColor } from 'react-native'; +import { findNodeHandle, processColor } from 'react-native'; import type { CameraApi } from './types'; import type { CameraProps } from './CameraProps'; - -const { RNCameraKitModule } = NativeModules; -const NativeCamera = requireNativeComponent('CKCameraManager'); +import NativeCamera from './specs/CameraNativeComponent'; +import NativeCameraKitModule from './specs/NativeCameraKitModule'; const Camera = React.forwardRef((props, ref) => { const nativeRef = React.useRef(null); React.useImperativeHandle(ref, () => ({ capture: async (options = {}) => { - // Because RN doesn't support return types for ViewManager methods - // we must use the general module and tell it what View it's supposed to be using - return await RNCameraKitModule.capture(options, findNodeHandle(nativeRef.current ?? null)); + return await NativeCameraKitModule.capture(options, findNodeHandle(nativeRef.current) ?? undefined); }, requestDeviceCameraAuthorization: () => { throw new Error('Not implemented'); @@ -28,6 +25,7 @@ const Camera = React.forwardRef((props, ref) => { transformedProps.frameColor = processColor(props.frameColor) as any; transformedProps.laserColor = processColor(props.laserColor) as any; + // @ts-expect-error props for codegen differ a bit from the user-facing ones return ; }); diff --git a/src/Camera.ios.tsx b/src/Camera.ios.tsx index ab10f5035..b1b1ee57b 100644 --- a/src/Camera.ios.tsx +++ b/src/Camera.ios.tsx @@ -1,10 +1,9 @@ import React from 'react'; -import { requireNativeComponent, NativeModules } from 'react-native'; +import { findNodeHandle } from 'react-native'; import type { CameraApi } from './types'; import type { CameraProps } from './CameraProps'; - -const { CKCameraManager } = NativeModules; -const NativeCamera = requireNativeComponent('CKCamera'); +import NativeCamera from './specs/CameraNativeComponent'; +import NativeCameraKitModule from './specs/NativeCameraKitModule'; const Camera = React.forwardRef((props, ref) => { const nativeRef = React.useRef(null); @@ -14,16 +13,17 @@ const Camera = React.forwardRef((props, ref) => { React.useImperativeHandle(ref, () => ({ capture: async () => { - return await CKCameraManager.capture({}); + return await NativeCameraKitModule.capture({}, findNodeHandle(nativeRef.current) ?? undefined); }, requestDeviceCameraAuthorization: async () => { - return await CKCameraManager.checkDeviceCameraAuthorizationStatus(); + return await NativeCameraKitModule.checkDeviceCameraAuthorizationStatus(); }, checkDeviceCameraAuthorizationStatus: async () => { - return await CKCameraManager.checkDeviceCameraAuthorizationStatus(); + return await NativeCameraKitModule.checkDeviceCameraAuthorizationStatus(); }, })); + // @ts-expect-error props for codegen differ a bit from the user-facing ones return ; }); diff --git a/src/specs/CameraNativeComponent.ts b/src/specs/CameraNativeComponent.ts new file mode 100644 index 000000000..90d649313 --- /dev/null +++ b/src/specs/CameraNativeComponent.ts @@ -0,0 +1,54 @@ +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import type { ViewProps, ColorValue } from 'react-native'; +import type { + DirectEventHandler, + Int32, + Double, +} from 'react-native/Libraries/Types/CodegenTypes'; + +type OnReadCodeData = { + codeStringValue: string; + codeFormat: string; +}; + +type OnOrientationChangeData = { + orientation: Int32; +}; + +type OnZoom = { + zoom: Double; +} + +export interface NativeProps extends ViewProps { + flashMode?: string; + focusMode?: string; + zoomMode?: string; + zoom?: Double; + maxZoom?: Double; + torchMode?: string; + cameraType?: string; + onOrientationChange?: DirectEventHandler; + onZoom?: DirectEventHandler; + onError?: DirectEventHandler<{errorMessage: string }>; + scanBarcode?: boolean; + showFrame?: boolean; + laserColor?: ColorValue; + frameColor?: ColorValue; + onReadCode?: DirectEventHandler; + ratioOverlay?: string; + ratioOverlayColor?: ColorValue; + resetFocusTimeout?: Int32; + resetFocusWhenMotionDetected?: boolean; + resizeMode?: string; + scanThrottleDelay?: Int32; + shutterPhotoSound?: boolean; + onCaptureButtonPressIn?: DirectEventHandler<{}>; + onCaptureButtonPressOut?: DirectEventHandler<{}>; + + // not mentioned in props but available on the native side + shutterAnimationDuration?: Int32; + outputPath?: string; + onPictureTaken?: DirectEventHandler<{uri: string}>; +} + +export default codegenNativeComponent('CKCamera'); diff --git a/src/specs/NativeCameraKitModule.ts b/src/specs/NativeCameraKitModule.ts new file mode 100644 index 000000000..f1b831ec6 --- /dev/null +++ b/src/specs/NativeCameraKitModule.ts @@ -0,0 +1,25 @@ +/* eslint-disable @typescript-eslint/ban-types */ +// its needed for codegen to work +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; +import type { Double, Int32, UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes'; + +type CaptureData = { + uri: string; + name: string; + height: Int32; + width: Int32; + // Android only + id?: string; + path?: string; + // iOS only + size?: Int32; +}; + +export interface Spec extends TurboModule { + capture(options?: UnsafeObject, tag?: Double): Promise; + requestDeviceCameraAuthorization: () => Promise; + checkDeviceCameraAuthorizationStatus: () => Promise; +} + +export default TurboModuleRegistry.getEnforcing('RNCameraKitModule');