@unif/react-native-hms-scan 0.1.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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/ReactNativeHmsScan.podspec +30 -0
  4. package/android/build.gradle +79 -0
  5. package/android/consumer-rules.pro +13 -0
  6. package/android/src/main/AndroidManifest.xml +23 -0
  7. package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanModule.kt +192 -0
  8. package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanResultMapper.kt +145 -0
  9. package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanView.kt +257 -0
  10. package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanViewManager.kt +66 -0
  11. package/android/src/main/java/com/unif/reactnativehmsscan/ReactNativeHmsScanPackage.kt +49 -0
  12. package/ios/HmsScanModule.h +20 -0
  13. package/ios/HmsScanModule.mm +161 -0
  14. package/ios/HmsScanResultMapper.h +45 -0
  15. package/ios/HmsScanResultMapper.mm +297 -0
  16. package/ios/HmsScanView.h +24 -0
  17. package/ios/HmsScanView.mm +322 -0
  18. package/lib/module/HmsScanView.js +43 -0
  19. package/lib/module/HmsScanView.js.map +1 -0
  20. package/lib/module/HmsScanViewNativeComponent.ts +44 -0
  21. package/lib/module/NativeHmsScan.js +11 -0
  22. package/lib/module/NativeHmsScan.js.map +1 -0
  23. package/lib/module/Scanner/DeniedOverlay.js +60 -0
  24. package/lib/module/Scanner/DeniedOverlay.js.map +1 -0
  25. package/lib/module/Scanner/ResultFail.js +79 -0
  26. package/lib/module/Scanner/ResultFail.js.map +1 -0
  27. package/lib/module/Scanner/ResultFocus.js +174 -0
  28. package/lib/module/Scanner/ResultFocus.js.map +1 -0
  29. package/lib/module/Scanner/ScanHint.js +44 -0
  30. package/lib/module/Scanner/ScanHint.js.map +1 -0
  31. package/lib/module/Scanner/ScanToolbar.js +103 -0
  32. package/lib/module/Scanner/ScanToolbar.js.map +1 -0
  33. package/lib/module/Scanner/ScanTopBar.js +87 -0
  34. package/lib/module/Scanner/ScanTopBar.js.map +1 -0
  35. package/lib/module/Scanner/Scanner.js +184 -0
  36. package/lib/module/Scanner/Scanner.js.map +1 -0
  37. package/lib/module/Scanner/Viewfinder.js +159 -0
  38. package/lib/module/Scanner/Viewfinder.js.map +1 -0
  39. package/lib/module/Scanner/scanChrome.js +23 -0
  40. package/lib/module/Scanner/scanChrome.js.map +1 -0
  41. package/lib/module/decodeImage.js +32 -0
  42. package/lib/module/decodeImage.js.map +1 -0
  43. package/lib/module/format.js +66 -0
  44. package/lib/module/format.js.map +1 -0
  45. package/lib/module/index.js +21 -0
  46. package/lib/module/index.js.map +1 -0
  47. package/lib/module/mock.js +30 -0
  48. package/lib/module/mock.js.map +1 -0
  49. package/lib/module/package.json +1 -0
  50. package/lib/module/permissions.js +28 -0
  51. package/lib/module/permissions.js.map +1 -0
  52. package/lib/module/types.js +46 -0
  53. package/lib/module/types.js.map +1 -0
  54. package/lib/typescript/jest.setup.d.ts +1 -0
  55. package/lib/typescript/jest.setup.d.ts.map +1 -0
  56. package/lib/typescript/package.json +1 -0
  57. package/lib/typescript/src/HmsScanView.d.ts +36 -0
  58. package/lib/typescript/src/HmsScanView.d.ts.map +1 -0
  59. package/lib/typescript/src/HmsScanViewNativeComponent.d.ts +32 -0
  60. package/lib/typescript/src/HmsScanViewNativeComponent.d.ts.map +1 -0
  61. package/lib/typescript/src/NativeHmsScan.d.ts +17 -0
  62. package/lib/typescript/src/NativeHmsScan.d.ts.map +1 -0
  63. package/lib/typescript/src/Scanner/DeniedOverlay.d.ts +7 -0
  64. package/lib/typescript/src/Scanner/DeniedOverlay.d.ts.map +1 -0
  65. package/lib/typescript/src/Scanner/ResultFail.d.ts +7 -0
  66. package/lib/typescript/src/Scanner/ResultFail.d.ts.map +1 -0
  67. package/lib/typescript/src/Scanner/ResultFocus.d.ts +12 -0
  68. package/lib/typescript/src/Scanner/ResultFocus.d.ts.map +1 -0
  69. package/lib/typescript/src/Scanner/ScanHint.d.ts +8 -0
  70. package/lib/typescript/src/Scanner/ScanHint.d.ts.map +1 -0
  71. package/lib/typescript/src/Scanner/ScanToolbar.d.ts +9 -0
  72. package/lib/typescript/src/Scanner/ScanToolbar.d.ts.map +1 -0
  73. package/lib/typescript/src/Scanner/ScanTopBar.d.ts +10 -0
  74. package/lib/typescript/src/Scanner/ScanTopBar.d.ts.map +1 -0
  75. package/lib/typescript/src/Scanner/Scanner.d.ts +34 -0
  76. package/lib/typescript/src/Scanner/Scanner.d.ts.map +1 -0
  77. package/lib/typescript/src/Scanner/Viewfinder.d.ts +11 -0
  78. package/lib/typescript/src/Scanner/Viewfinder.d.ts.map +1 -0
  79. package/lib/typescript/src/Scanner/scanChrome.d.ts +19 -0
  80. package/lib/typescript/src/Scanner/scanChrome.d.ts.map +1 -0
  81. package/lib/typescript/src/decodeImage.d.ts +12 -0
  82. package/lib/typescript/src/decodeImage.d.ts.map +1 -0
  83. package/lib/typescript/src/format.d.ts +13 -0
  84. package/lib/typescript/src/format.d.ts.map +1 -0
  85. package/lib/typescript/src/index.d.ts +10 -0
  86. package/lib/typescript/src/index.d.ts.map +1 -0
  87. package/lib/typescript/src/mock.d.ts +12 -0
  88. package/lib/typescript/src/mock.d.ts.map +1 -0
  89. package/lib/typescript/src/permissions.d.ts +9 -0
  90. package/lib/typescript/src/permissions.d.ts.map +1 -0
  91. package/lib/typescript/src/types.d.ts +62 -0
  92. package/lib/typescript/src/types.d.ts.map +1 -0
  93. package/package.json +174 -0
  94. package/src/HmsScanView.tsx +87 -0
  95. package/src/HmsScanViewNativeComponent.ts +44 -0
  96. package/src/NativeHmsScan.ts +24 -0
  97. package/src/Scanner/DeniedOverlay.tsx +59 -0
  98. package/src/Scanner/ResultFail.tsx +71 -0
  99. package/src/Scanner/ResultFocus.tsx +118 -0
  100. package/src/Scanner/ScanHint.tsx +38 -0
  101. package/src/Scanner/ScanToolbar.tsx +93 -0
  102. package/src/Scanner/ScanTopBar.tsx +74 -0
  103. package/src/Scanner/Scanner.tsx +238 -0
  104. package/src/Scanner/Viewfinder.tsx +115 -0
  105. package/src/Scanner/scanChrome.ts +20 -0
  106. package/src/decodeImage.ts +36 -0
  107. package/src/format.ts +93 -0
  108. package/src/index.tsx +35 -0
  109. package/src/mock.ts +43 -0
  110. package/src/permissions.ts +27 -0
  111. package/src/types.ts +138 -0
@@ -0,0 +1,257 @@
1
+ package com.unif.reactnativehmsscan
2
+
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import android.os.Bundle
6
+ import android.widget.FrameLayout
7
+ import com.facebook.react.bridge.Arguments
8
+ import com.facebook.react.bridge.LifecycleEventListener
9
+ import com.facebook.react.bridge.WritableMap
10
+ import com.facebook.react.uimanager.ThemedReactContext
11
+ import com.facebook.react.uimanager.UIManagerHelper
12
+ import com.facebook.react.uimanager.events.Event
13
+ import com.huawei.hms.hmsscankit.OnLightVisibleCallBack
14
+ import com.huawei.hms.hmsscankit.OnResultCallback
15
+ import com.huawei.hms.hmsscankit.RemoteView
16
+ import com.huawei.hms.ml.scan.HmsScan
17
+
18
+ /**
19
+ * Fabric view "HmsScanView". A FrameLayout that hosts HUAWEI Scan Kit's
20
+ * [RemoteView] (a blank camera preview). Scanning UI (viewfinder, torch button,
21
+ * etc.) is drawn on top by the JS layer; this view only renders the preview and
22
+ * emits scan/torch/error events.
23
+ *
24
+ * RemoteView is built lazily on first attach (so all initial props are known) and
25
+ * rebuilt when its build-time props (formatsCsv / continuous) change. Lifecycle is
26
+ * forwarded both from the View attach/detach callbacks and from the host
27
+ * Activity's lifecycle via [LifecycleEventListener].
28
+ */
29
+ class HmsScanView(context: Context) :
30
+ FrameLayout(context), LifecycleEventListener {
31
+
32
+ private val themedReactContext: ThemedReactContext = context as ThemedReactContext
33
+
34
+ private var remoteView: RemoteView? = null
35
+
36
+ // Build-time props (require a RemoteView rebuild when changed).
37
+ private var formatsCsv: String = ""
38
+ private var continuous: Boolean = true
39
+
40
+ // Runtime props (applied to the live RemoteView; stashed until it exists).
41
+ private var paused: Boolean = false
42
+ private var torch: Boolean = false
43
+
44
+ // True while this view is between onAttachedToWindow and onDetachedFromWindow.
45
+ private var attached: Boolean = false
46
+
47
+ init {
48
+ themedReactContext.addLifecycleEventListener(this)
49
+ }
50
+
51
+ // ── Props (called from the ViewManager) ───────────────────────────────────
52
+
53
+ fun setFormatsCsv(value: String?) {
54
+ val next = value ?: ""
55
+ if (next == formatsCsv && remoteView != null) return
56
+ formatsCsv = next
57
+ rebuildIfAttached()
58
+ }
59
+
60
+ fun setContinuous(value: Boolean) {
61
+ if (value == continuous && remoteView != null) return
62
+ continuous = value
63
+ rebuildIfAttached()
64
+ }
65
+
66
+ fun setPaused(value: Boolean) {
67
+ paused = value
68
+ applyPaused()
69
+ }
70
+
71
+ fun setTorch(value: Boolean) {
72
+ torch = value
73
+ applyTorch()
74
+ }
75
+
76
+ // ── View lifecycle ────────────────────────────────────────────────────────
77
+
78
+ override fun onAttachedToWindow() {
79
+ super.onAttachedToWindow()
80
+ attached = true
81
+ if (remoteView == null) {
82
+ buildRemoteView()
83
+ }
84
+ remoteView?.let {
85
+ it.onStart()
86
+ it.onResume()
87
+ }
88
+ }
89
+
90
+ override fun onDetachedFromWindow() {
91
+ super.onDetachedFromWindow()
92
+ attached = false
93
+ teardownRemoteView()
94
+ }
95
+
96
+ // ── Host (Activity) lifecycle ─────────────────────────────────────────────
97
+
98
+ override fun onHostResume() {
99
+ if (attached) {
100
+ remoteView?.onResume()
101
+ }
102
+ }
103
+
104
+ override fun onHostPause() {
105
+ remoteView?.onPause()
106
+ }
107
+
108
+ override fun onHostDestroy() {
109
+ teardownRemoteView()
110
+ themedReactContext.removeLifecycleEventListener(this)
111
+ }
112
+
113
+ // ── RemoteView build / teardown ───────────────────────────────────────────
114
+
115
+ private fun buildRemoteView() {
116
+ val activity = resolveActivity()
117
+ if (activity == null) {
118
+ emitError(
119
+ "E_CAMERA_INIT",
120
+ "Cannot start camera: no host Activity available for RemoteView"
121
+ )
122
+ return
123
+ }
124
+
125
+ try {
126
+ val builder = RemoteView.Builder()
127
+ .setContext(activity)
128
+ .setContinuouslyScan(continuous)
129
+
130
+ val types = HmsScanResultMapper.parseFormatsCsv(formatsCsv)
131
+ if (types != null && types.isNotEmpty()) {
132
+ val first = types.first()
133
+ val rest = if (types.size > 1) types.copyOfRange(1, types.size) else IntArray(0)
134
+ builder.setFormat(first, *rest)
135
+ } else {
136
+ builder.setFormat(HmsScan.ALL_SCAN_TYPE)
137
+ }
138
+
139
+ val view = builder.build()
140
+
141
+ view.setOnResultCallback(OnResultCallback { result -> onScanResult(result) })
142
+ view.setOnLightVisibleCallback(
143
+ OnLightVisibleCallBack { visible -> onTorchVisible(visible) }
144
+ )
145
+
146
+ // onCreate must run after build() and before addView (per HMS docs/demo).
147
+ view.onCreate(Bundle())
148
+ addView(
149
+ view,
150
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
151
+ )
152
+
153
+ remoteView = view
154
+
155
+ // Apply runtime props that may have arrived before the view existed.
156
+ applyPaused()
157
+ applyTorch()
158
+ } catch (e: Throwable) {
159
+ emitError("E_CAMERA_INIT", e.message ?: "Failed to initialize camera")
160
+ }
161
+ }
162
+
163
+ private fun teardownRemoteView() {
164
+ val view = remoteView ?: return
165
+ try {
166
+ view.onPause()
167
+ view.onStop()
168
+ view.onDestroy()
169
+ } catch (_: Throwable) {
170
+ // Defensive: never let teardown crash the host.
171
+ }
172
+ removeView(view)
173
+ remoteView = null
174
+ }
175
+
176
+ /** Rebuild the RemoteView in place to honour a changed build-time prop. */
177
+ private fun rebuildIfAttached() {
178
+ if (!attached) return
179
+ teardownRemoteView()
180
+ buildRemoteView()
181
+ remoteView?.let {
182
+ it.onStart()
183
+ it.onResume()
184
+ }
185
+ }
186
+
187
+ private fun applyPaused() {
188
+ val view = remoteView ?: return
189
+ try {
190
+ if (paused) {
191
+ view.pauseContinuouslyScan()
192
+ } else {
193
+ view.resumeContinuouslyScan()
194
+ }
195
+ } catch (_: Throwable) {
196
+ // Ignore: only meaningful in continuous mode.
197
+ }
198
+ }
199
+
200
+ private fun applyTorch() {
201
+ val view = remoteView ?: return
202
+ try {
203
+ // switchLight() toggles; only flip when the current state differs from target.
204
+ if (view.lightStatus != torch) {
205
+ view.switchLight()
206
+ }
207
+ } catch (_: Throwable) {
208
+ // Ignore: device may have no flash.
209
+ }
210
+ }
211
+
212
+ /** Resolve the hosting Activity required by RemoteView.Builder.setContext(). */
213
+ private fun resolveActivity(): Activity? = themedReactContext.currentActivity
214
+
215
+ // ── Scan Kit callbacks → Fabric events ────────────────────────────────────
216
+
217
+ private fun onScanResult(result: Array<HmsScan?>?) {
218
+ // Drop empty callbacks (continuous mode can fire with nothing useful).
219
+ if (result == null || result.isEmpty()) return
220
+ val json = HmsScanResultMapper.toJson(result)
221
+ // toJson skips value-less hits; avoid emitting an empty "[]" event.
222
+ if (json == "[]") return
223
+ emitEvent("topScanResult", Arguments.createMap().apply {
224
+ putString("resultsJson", json)
225
+ })
226
+ }
227
+
228
+ private fun onTorchVisible(visible: Boolean) {
229
+ // Keep our cached torch flag in sync with the actual hardware state.
230
+ val on = remoteView?.lightStatus ?: torch
231
+ torch = on
232
+ emitEvent("topTorchStatus", Arguments.createMap().apply {
233
+ putBoolean("available", visible)
234
+ putBoolean("on", on)
235
+ })
236
+ }
237
+
238
+ private fun emitError(code: String, message: String) {
239
+ emitEvent("topScanError", Arguments.createMap().apply {
240
+ putString("code", code)
241
+ putString("message", message)
242
+ })
243
+ }
244
+
245
+ // ── Fabric event dispatch ──────────────────────────────────────────────────
246
+
247
+ private fun emitEvent(eventName: String, payload: WritableMap) {
248
+ val reactTag = id
249
+ val dispatcher =
250
+ UIManagerHelper.getEventDispatcherForReactTag(themedReactContext, reactTag)
251
+ val surfaceId = themedReactContext.surfaceId
252
+ dispatcher?.dispatchEvent(object : Event<Event<*>>(surfaceId, reactTag) {
253
+ override fun getEventName(): String = eventName
254
+ override fun getEventData(): WritableMap = payload
255
+ })
256
+ }
257
+ }
@@ -0,0 +1,66 @@
1
+ package com.unif.reactnativehmsscan
2
+
3
+ import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.SimpleViewManager
5
+ import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.ViewManagerDelegate
7
+ import com.facebook.react.uimanager.annotations.ReactProp
8
+ import com.facebook.react.viewmanagers.HmsScanViewManagerInterface
9
+ import com.facebook.react.viewmanagers.HmsScanViewManagerDelegate
10
+
11
+ /**
12
+ * Fabric ViewManager for "HmsScanView". Delegates prop dispatch to the
13
+ * codegen-generated [HmsScanViewManagerDelegate] and forwards each prop to the
14
+ * underlying [HmsScanView].
15
+ */
16
+ @ReactModule(name = HmsScanViewManager.NAME)
17
+ class HmsScanViewManager :
18
+ SimpleViewManager<HmsScanView>(),
19
+ HmsScanViewManagerInterface<HmsScanView> {
20
+
21
+ private val mDelegate: ViewManagerDelegate<HmsScanView> =
22
+ HmsScanViewManagerDelegate(this)
23
+
24
+ override fun getDelegate(): ViewManagerDelegate<HmsScanView> = mDelegate
25
+
26
+ override fun getName(): String = NAME
27
+
28
+ public override fun createViewInstance(context: ThemedReactContext): HmsScanView =
29
+ HmsScanView(context)
30
+
31
+ @ReactProp(name = "formatsCsv")
32
+ override fun setFormatsCsv(view: HmsScanView?, value: String?) {
33
+ view?.setFormatsCsv(value)
34
+ }
35
+
36
+ @ReactProp(name = "continuous", defaultBoolean = true)
37
+ override fun setContinuous(view: HmsScanView?, value: Boolean) {
38
+ view?.setContinuous(value)
39
+ }
40
+
41
+ @ReactProp(name = "paused", defaultBoolean = false)
42
+ override fun setPaused(view: HmsScanView?, value: Boolean) {
43
+ view?.setPaused(value)
44
+ }
45
+
46
+ @ReactProp(name = "torch", defaultBoolean = false)
47
+ override fun setTorch(view: HmsScanView?, value: Boolean) {
48
+ view?.setTorch(value)
49
+ }
50
+
51
+ /**
52
+ * Map internal Fabric event names to the JS prop callback names declared in
53
+ * HmsScanViewNativeComponent.ts.
54
+ */
55
+ override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> {
56
+ return mapOf(
57
+ "topScanResult" to mapOf("registrationName" to "onScanResult"),
58
+ "topScanError" to mapOf("registrationName" to "onScanError"),
59
+ "topTorchStatus" to mapOf("registrationName" to "onTorchStatus")
60
+ )
61
+ }
62
+
63
+ companion object {
64
+ const val NAME = "HmsScanView"
65
+ }
66
+ }
@@ -0,0 +1,49 @@
1
+ package com.unif.reactnativehmsscan
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import com.facebook.react.uimanager.ViewManager
9
+
10
+ /**
11
+ * React Native package wiring for @unif/react-native-hms-scan:
12
+ * - the "HmsScan" TurboModule ([HmsScanModule]) —— 图片识别 + 相机权限。
13
+ * - the "HmsScanView" Fabric 视图 ([HmsScanViewManager]) —— 定制视图扫码。
14
+ *
15
+ * 结构对齐官方 create-react-native-library 模板:模块走 getModule /
16
+ * getReactModuleInfoProvider,视图走 createViewManagers + listOf(ViewManager)。
17
+ */
18
+ class ReactNativeHmsScanPackage : BaseReactPackage() {
19
+
20
+ override fun getModule(
21
+ name: String,
22
+ reactContext: ReactApplicationContext
23
+ ): NativeModule? {
24
+ return if (name == HmsScanModule.NAME) {
25
+ HmsScanModule(reactContext)
26
+ } else {
27
+ null
28
+ }
29
+ }
30
+
31
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
32
+ mapOf(
33
+ HmsScanModule.NAME to ReactModuleInfo(
34
+ name = HmsScanModule.NAME,
35
+ className = HmsScanModule.NAME,
36
+ canOverrideExistingModule = false,
37
+ needsEagerInit = false,
38
+ isCxxModule = false,
39
+ isTurboModule = true
40
+ )
41
+ )
42
+ }
43
+
44
+ override fun createViewManagers(
45
+ reactContext: ReactApplicationContext
46
+ ): List<ViewManager<*, *>> {
47
+ return listOf(HmsScanViewManager())
48
+ }
49
+ }
@@ -0,0 +1,20 @@
1
+ //
2
+ // HmsScanModule.h
3
+ // @unif/react-native-hms-scan
4
+ //
5
+ // TurboModule registered under the name "HmsScan". Implements the codegen
6
+ // protocol NativeHmsScanSpec (generated from src/NativeHmsScan.ts):
7
+ // - decodeImage:formatsCsv:resolve:reject:
8
+ // - getCameraPermissionStatus:reject:
9
+ // - requestCameraPermission:reject:
10
+ //
11
+
12
+ #import <ReactNativeHmsScanSpec/ReactNativeHmsScanSpec.h>
13
+
14
+ NS_ASSUME_NONNULL_BEGIN
15
+
16
+ @interface HmsScanModule : NSObject <NativeHmsScanSpec>
17
+
18
+ @end
19
+
20
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,161 @@
1
+ //
2
+ // HmsScanModule.mm
3
+ // @unif/react-native-hms-scan
4
+ //
5
+
6
+ #import "HmsScanModule.h"
7
+ #import "HmsScanResultMapper.h"
8
+
9
+ #import <AVFoundation/AVFoundation.h>
10
+ #import <UIKit/UIKit.h>
11
+
12
+ #import <ScanKitFrameWork/ScanKitFrameWork.h>
13
+
14
+ // Error codes mirror HmsScanErrorCode in src/types.ts.
15
+ static NSString *const kErrImageLoadFailed = @"E_IMAGE_LOAD_FAILED";
16
+ static NSString *const kErrDecodeFailed = @"E_DECODE_FAILED";
17
+
18
+ @implementation HmsScanModule
19
+
20
+ // Export under the JS-visible name "HmsScan" (TurboModuleRegistry.getEnforcing<Spec>('HmsScan')).
21
+ RCT_EXPORT_MODULE(HmsScan)
22
+
23
+ #pragma mark - Permission status mapping
24
+
25
+ // AVAuthorizationStatus -> our CameraPermissionStatus string.
26
+ // authorized -> granted; notDetermined -> undetermined; denied/restricted -> blocked.
27
+ + (NSString *)stringForAuthorizationStatus:(AVAuthorizationStatus)status
28
+ {
29
+ switch (status) {
30
+ case AVAuthorizationStatusAuthorized:
31
+ return @"granted";
32
+ case AVAuthorizationStatusNotDetermined:
33
+ return @"undetermined";
34
+ case AVAuthorizationStatusDenied:
35
+ case AVAuthorizationStatusRestricted:
36
+ return @"blocked";
37
+ default:
38
+ return @"undetermined";
39
+ }
40
+ }
41
+
42
+ - (void)getCameraPermissionStatus:(RCTPromiseResolveBlock)resolve
43
+ reject:(RCTPromiseRejectBlock)reject
44
+ {
45
+ AVAuthorizationStatus status =
46
+ [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
47
+ resolve([HmsScanModule stringForAuthorizationStatus:status]);
48
+ }
49
+
50
+ - (void)requestCameraPermission:(RCTPromiseResolveBlock)resolve
51
+ reject:(RCTPromiseRejectBlock)reject
52
+ {
53
+ AVAuthorizationStatus current =
54
+ [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
55
+
56
+ // Only notDetermined can present the system prompt. For an already-resolved
57
+ // state, return it directly (requestAccess would still call back immediately,
58
+ // but this avoids any ambiguity and keeps the mapping exact).
59
+ if (current != AVAuthorizationStatusNotDetermined) {
60
+ resolve([HmsScanModule stringForAuthorizationStatus:current]);
61
+ return;
62
+ }
63
+
64
+ [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo
65
+ completionHandler:^(BOOL granted) {
66
+ // Re-read the authoritative status rather than inferring from `granted`,
67
+ // so restricted/denied are reported correctly.
68
+ AVAuthorizationStatus updated =
69
+ [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
70
+ resolve([HmsScanModule stringForAuthorizationStatus:updated]);
71
+ }];
72
+ }
73
+
74
+ #pragma mark - Image loading
75
+
76
+ // Load a UIImage from a local URI: file:// , an absolute/relative file path,
77
+ // or a data: URI. Remote URLs and Photos (ph:// / assets-library://) are not
78
+ // supported here -> returns nil so the caller rejects E_IMAGE_LOAD_FAILED.
79
+ + (nullable UIImage *)imageForURI:(NSString *)uri
80
+ {
81
+ if (uri.length == 0) {
82
+ return nil;
83
+ }
84
+
85
+ NSURL *url = [NSURL URLWithString:uri];
86
+ NSString *scheme = url.scheme.lowercaseString;
87
+
88
+ // data: URI (base64 or otherwise) -> decode bytes directly.
89
+ if ([scheme isEqualToString:@"data"]) {
90
+ NSData *data = [NSData dataWithContentsOfURL:url];
91
+ return data ? [UIImage imageWithData:data] : nil;
92
+ }
93
+
94
+ // file:// URI -> use its filesystem path.
95
+ if ([scheme isEqualToString:@"file"]) {
96
+ NSString *path = url.path;
97
+ return path ? [UIImage imageWithContentsOfFile:path] : nil;
98
+ }
99
+
100
+ // No scheme (or a Windows-like drive letter is irrelevant on iOS): treat the
101
+ // whole string as a filesystem path. Also strip a leading file path that may
102
+ // have arrived percent-encoded.
103
+ if (scheme == nil || scheme.length == 0) {
104
+ UIImage *image = [UIImage imageWithContentsOfFile:uri];
105
+ if (image == nil) {
106
+ NSString *decoded = [uri stringByRemovingPercentEncoding];
107
+ if (decoded != nil && ![decoded isEqualToString:uri]) {
108
+ image = [UIImage imageWithContentsOfFile:decoded];
109
+ }
110
+ }
111
+ return image;
112
+ }
113
+
114
+ // ph:// , assets-library:// , http(s):// , content:// (Android-only) etc. are
115
+ // intentionally unsupported on iOS in this bridge.
116
+ return nil;
117
+ }
118
+
119
+ #pragma mark - decodeImage
120
+
121
+ - (void)decodeImage:(NSString *)uri
122
+ formatsCsv:(NSString *)formatsCsv
123
+ resolve:(RCTPromiseResolveBlock)resolve
124
+ reject:(RCTPromiseRejectBlock)reject
125
+ {
126
+ UIImage *image = [HmsScanModule imageForURI:uri];
127
+ if (image == nil) {
128
+ reject(kErrImageLoadFailed,
129
+ [NSString stringWithFormat:@"无法从 URI 加载图片(仅支持 file:// / 绝对路径 / data:):%@",
130
+ uri ?: @"(nil)"],
131
+ nil);
132
+ return;
133
+ }
134
+
135
+ @try {
136
+ unsigned int formatType = [HmsScanResultMapper scanFormatTypeFromCsv:formatsCsv];
137
+ // photoMode/Photo=YES: still-image (album) decode path, per HUAWEI guidance.
138
+ HmsScanOptions *options =
139
+ [[HmsScanOptions alloc] initWithScanFormatType:formatType Photo:YES];
140
+
141
+ NSArray *raw = [HmsBitMap multiDecodeBitMapForImage:image withOptions:options];
142
+ NSString *json = [HmsScanResultMapper jsonStringFromHuaweiArray:raw];
143
+ // Always resolve a JSON array string (may be "[]" when nothing was found);
144
+ // JS parseResultsJson handles the empty case.
145
+ resolve(json);
146
+ } @catch (NSException *exception) {
147
+ reject(kErrDecodeFailed,
148
+ exception.reason ?: @"HUAWEI Scan Kit 解码图片时发生异常",
149
+ nil);
150
+ }
151
+ }
152
+
153
+ #pragma mark - TurboModule
154
+
155
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
156
+ (const facebook::react::ObjCTurboModule::InitParams &)params
157
+ {
158
+ return std::make_shared<facebook::react::NativeHmsScanSpecJSI>(params);
159
+ }
160
+
161
+ @end
@@ -0,0 +1,45 @@
1
+ //
2
+ // HmsScanResultMapper.h
3
+ // @unif/react-native-hms-scan
4
+ //
5
+ // Converts HUAWEI Scan Kit result dictionaries into the library's stable
6
+ // ScanResult[] JSON contract (shared by the TurboModule's decodeImage and the
7
+ // Fabric view's onScanResult event). The JSON shape is:
8
+ //
9
+ // [{"value":"...","format":"EAN_13","contentType":"ARTICLE",
10
+ // "cornerPoints":[{"x":10,"y":20}]}]
11
+ //
12
+ // Only `value` and `format` are guaranteed; `contentType` / `cornerPoints` are
13
+ // emitted only when derivable. See README of NativeHmsScan.ts for the contract.
14
+ //
15
+
16
+ #import <Foundation/Foundation.h>
17
+
18
+ NS_ASSUME_NONNULL_BEGIN
19
+
20
+ @interface HmsScanResultMapper : NSObject
21
+
22
+ /// Parse a comma-separated list of our BarcodeFormat strings into the
23
+ /// `HmsScanOptions.scanFormatType` bitmask (HMSScanFormatTypeCode OR-ed).
24
+ /// An empty/whitespace-only string means "all formats".
25
+ + (unsigned int)scanFormatTypeFromCsv:(nullable NSString *)csv;
26
+
27
+ /// Map a single HUAWEI result dictionary (keys: text / formatValue / sceneType /
28
+ /// ResultPoint ...) into one ScanResult dictionary, or nil when there is no
29
+ /// usable `text`. Returned dictionary is JSON-serializable.
30
+ + (nullable NSDictionary *)scanResultFromHuaweiDict:(nullable NSDictionary *)dict;
31
+
32
+ /// Map an array of HUAWEI result dictionaries into an array of ScanResult
33
+ /// dictionaries (dropping entries without a usable value).
34
+ + (NSArray<NSDictionary *> *)scanResultsFromHuaweiArray:(nullable NSArray *)array;
35
+
36
+ /// Serialize an array of ScanResult dictionaries to a JSON string.
37
+ /// Never returns nil: on failure it returns "[]".
38
+ + (NSString *)jsonStringFromScanResults:(nullable NSArray<NSDictionary *> *)results;
39
+
40
+ /// Convenience: HUAWEI multi-decode array -> ScanResult[] JSON string.
41
+ + (NSString *)jsonStringFromHuaweiArray:(nullable NSArray *)array;
42
+
43
+ @end
44
+
45
+ NS_ASSUME_NONNULL_END