@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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/ReactNativeHmsScan.podspec +30 -0
- package/android/build.gradle +79 -0
- package/android/consumer-rules.pro +13 -0
- package/android/src/main/AndroidManifest.xml +23 -0
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanModule.kt +192 -0
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanResultMapper.kt +145 -0
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanView.kt +257 -0
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanViewManager.kt +66 -0
- package/android/src/main/java/com/unif/reactnativehmsscan/ReactNativeHmsScanPackage.kt +49 -0
- package/ios/HmsScanModule.h +20 -0
- package/ios/HmsScanModule.mm +161 -0
- package/ios/HmsScanResultMapper.h +45 -0
- package/ios/HmsScanResultMapper.mm +297 -0
- package/ios/HmsScanView.h +24 -0
- package/ios/HmsScanView.mm +322 -0
- package/lib/module/HmsScanView.js +43 -0
- package/lib/module/HmsScanView.js.map +1 -0
- package/lib/module/HmsScanViewNativeComponent.ts +44 -0
- package/lib/module/NativeHmsScan.js +11 -0
- package/lib/module/NativeHmsScan.js.map +1 -0
- package/lib/module/Scanner/DeniedOverlay.js +60 -0
- package/lib/module/Scanner/DeniedOverlay.js.map +1 -0
- package/lib/module/Scanner/ResultFail.js +79 -0
- package/lib/module/Scanner/ResultFail.js.map +1 -0
- package/lib/module/Scanner/ResultFocus.js +174 -0
- package/lib/module/Scanner/ResultFocus.js.map +1 -0
- package/lib/module/Scanner/ScanHint.js +44 -0
- package/lib/module/Scanner/ScanHint.js.map +1 -0
- package/lib/module/Scanner/ScanToolbar.js +103 -0
- package/lib/module/Scanner/ScanToolbar.js.map +1 -0
- package/lib/module/Scanner/ScanTopBar.js +87 -0
- package/lib/module/Scanner/ScanTopBar.js.map +1 -0
- package/lib/module/Scanner/Scanner.js +184 -0
- package/lib/module/Scanner/Scanner.js.map +1 -0
- package/lib/module/Scanner/Viewfinder.js +159 -0
- package/lib/module/Scanner/Viewfinder.js.map +1 -0
- package/lib/module/Scanner/scanChrome.js +23 -0
- package/lib/module/Scanner/scanChrome.js.map +1 -0
- package/lib/module/decodeImage.js +32 -0
- package/lib/module/decodeImage.js.map +1 -0
- package/lib/module/format.js +66 -0
- package/lib/module/format.js.map +1 -0
- package/lib/module/index.js +21 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/mock.js +30 -0
- package/lib/module/mock.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/permissions.js +28 -0
- package/lib/module/permissions.js.map +1 -0
- package/lib/module/types.js +46 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/jest.setup.d.ts +1 -0
- package/lib/typescript/jest.setup.d.ts.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/HmsScanView.d.ts +36 -0
- package/lib/typescript/src/HmsScanView.d.ts.map +1 -0
- package/lib/typescript/src/HmsScanViewNativeComponent.d.ts +32 -0
- package/lib/typescript/src/HmsScanViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/NativeHmsScan.d.ts +17 -0
- package/lib/typescript/src/NativeHmsScan.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/DeniedOverlay.d.ts +7 -0
- package/lib/typescript/src/Scanner/DeniedOverlay.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ResultFail.d.ts +7 -0
- package/lib/typescript/src/Scanner/ResultFail.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ResultFocus.d.ts +12 -0
- package/lib/typescript/src/Scanner/ResultFocus.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ScanHint.d.ts +8 -0
- package/lib/typescript/src/Scanner/ScanHint.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ScanToolbar.d.ts +9 -0
- package/lib/typescript/src/Scanner/ScanToolbar.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ScanTopBar.d.ts +10 -0
- package/lib/typescript/src/Scanner/ScanTopBar.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/Scanner.d.ts +34 -0
- package/lib/typescript/src/Scanner/Scanner.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/Viewfinder.d.ts +11 -0
- package/lib/typescript/src/Scanner/Viewfinder.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/scanChrome.d.ts +19 -0
- package/lib/typescript/src/Scanner/scanChrome.d.ts.map +1 -0
- package/lib/typescript/src/decodeImage.d.ts +12 -0
- package/lib/typescript/src/decodeImage.d.ts.map +1 -0
- package/lib/typescript/src/format.d.ts +13 -0
- package/lib/typescript/src/format.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +10 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/mock.d.ts +12 -0
- package/lib/typescript/src/mock.d.ts.map +1 -0
- package/lib/typescript/src/permissions.d.ts +9 -0
- package/lib/typescript/src/permissions.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +62 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +174 -0
- package/src/HmsScanView.tsx +87 -0
- package/src/HmsScanViewNativeComponent.ts +44 -0
- package/src/NativeHmsScan.ts +24 -0
- package/src/Scanner/DeniedOverlay.tsx +59 -0
- package/src/Scanner/ResultFail.tsx +71 -0
- package/src/Scanner/ResultFocus.tsx +118 -0
- package/src/Scanner/ScanHint.tsx +38 -0
- package/src/Scanner/ScanToolbar.tsx +93 -0
- package/src/Scanner/ScanTopBar.tsx +74 -0
- package/src/Scanner/Scanner.tsx +238 -0
- package/src/Scanner/Viewfinder.tsx +115 -0
- package/src/Scanner/scanChrome.ts +20 -0
- package/src/decodeImage.ts +36 -0
- package/src/format.ts +93 -0
- package/src/index.tsx +35 -0
- package/src/mock.ts +43 -0
- package/src/permissions.ts +27 -0
- 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
|