@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 unif-design
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # @unif/react-native-hms-scan
2
+
3
+ 华为 **HMS 统一扫码服务**(HUAWEI Scan Kit)的 React Native 封装,新架构(TurboModule + Fabric)。聚焦两件事:
4
+
5
+ - **定制视图扫码** —— 底层 headless `<HmsScanView>` 相机组件,外加按设计稿做好的成品 `<Scanner>` 扫一扫页(聚焦款,浅色)。
6
+ - **图片识别** —— `decodeImage(uri)` 从本地图片解码条码 / 二维码。
7
+
8
+ > Android 用 **Scan SDK-Plus**(内置引擎,**不依赖设备安装 HMS Core APK**,非华为机型可用);iOS 用 CocoaPods 的 **ScanKitFrameWork**。两端**都不需要 AppGallery Connect / agconnect 配置 / API Key**。
9
+
10
+ ## 安装
11
+
12
+ ```sh
13
+ yarn add @unif/react-native-hms-scan react-native-svg
14
+ cd ios && pod install
15
+ ```
16
+
17
+ `react-native-svg` 是 peer 依赖(`<Scanner>` 的图标用它绘制)。本库为 New Architecture 库,宿主需开启新架构。
18
+
19
+ ### Android
20
+
21
+ 库的 gradle 已自带华为 maven 仓库与 `com.huawei.hms:scanplus` 依赖,**无需改宿主 gradle**。只需在宿主 `AndroidManifest.xml` 声明权限:
22
+
23
+ ```xml
24
+ <uses-permission android:name="android.permission.CAMERA" />
25
+ <uses-feature android:name="android.hardware.camera" android:required="false" />
26
+ <!-- 仅当用 decodeImage 解相册图时需要 -->
27
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- API 33+ -->
28
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
29
+ android:maxSdkVersion="32" />
30
+ ```
31
+
32
+ - minSdkVersion ≥ 24。
33
+ - **不需要** `agconnect-services.json` / agconnect 插件 / API Key。
34
+
35
+ ### iOS
36
+
37
+ `pod install` 会自动拉取 `ScanKitFrameWork`。在宿主 `Info.plist` 加:
38
+
39
+ ```xml
40
+ <key>NSCameraUsageDescription</key>
41
+ <string>用于扫描商品条码与门店二维码</string>
42
+ <!-- 仅当用 decodeImage 解相册图时需要 -->
43
+ <key>NSPhotoLibraryUsageDescription</key>
44
+ <string>用于从相册选取图片识别条码</string>
45
+ ```
46
+
47
+ - 最低 iOS 版本随宿主 RN 工程。
48
+ - `ScanKitFrameWork` 是 **arm64-only 静态库**:podspec 已对 Apple 芯片模拟器排掉 arm64(模拟器走 x86_64),**真机 arm64 不受影响**;建议在真机调试扫码。
49
+ - **不需要** AppGallery Connect / API Key。
50
+
51
+ ## 用法
52
+
53
+ ### 1) 成品扫一扫页 `<Scanner>`(推荐)
54
+
55
+ ```tsx
56
+ import { Scanner, type ScanResult, type ScanProduct } from '@unif/react-native-hms-scan';
57
+
58
+ function ScanScreen({ navigation }) {
59
+ return (
60
+ <Scanner
61
+ title="扫一扫"
62
+ onClose={() => navigation.goBack()}
63
+ // 扫到条码后解析商品(你的业务:查接口/本地库)。返回 null = 未识别。
64
+ resolveProduct={async (r: ScanResult): Promise<ScanProduct | null> => {
65
+ const p = await api.lookupByBarcode(r.value);
66
+ return p && {
67
+ name: p.name, brand: p.brand, price: `¥${p.price}`,
68
+ spec: p.spec, stockShort: p.stockText, barcode: r.value,
69
+ };
70
+ }}
71
+ // 点"确认":把结果带回上一级
72
+ onConfirm={(product, result) => {
73
+ navigation.navigate('Order', { barcode: result.value, product });
74
+ }}
75
+ // 点"相册":用你自己的图片选择器返回本地 uri
76
+ pickImage={async () => {
77
+ const res = await launchImageLibrary({ mediaType: 'photo' });
78
+ return res.assets?.[0]?.uri ?? null;
79
+ }}
80
+ />
81
+ );
82
+ }
83
+ ```
84
+
85
+ `<Scanner>` 内部状态:取景 → 识别中 → 识别成功(浮层确认卡)/ 未识别(重试弹层)/ 无权限(去设置)。单次扫一个,确认后带回上一级。手电筒 / 相册在底部工具栏。
86
+
87
+ > 安全区:默认 `topInset=54 / bottomInset=34`。若用 `react-native-safe-area-context`,把 `insets.top/bottom` 传进来更精准。
88
+
89
+ ### 2) 底层 headless 组件 `<HmsScanView>`
90
+
91
+ 完全自定义 UI 时,只用相机预览 + 结果事件,自己画取景框:
92
+
93
+ ```tsx
94
+ import { HmsScanView, type ScanResult } from '@unif/react-native-hms-scan';
95
+
96
+ <HmsScanView
97
+ style={{ flex: 1 }}
98
+ formats={['QR_CODE', 'EAN_13']} // 不传 = 全部码制
99
+ torch={torchOn} // Android 可控;iOS 为尽力而为
100
+ paused={paused} // 命中后想停在结果时置 true
101
+ onScanResult={(results: ScanResult[]) => {
102
+ const code = results[0]?.value;
103
+ }}
104
+ onScanError={(e) => console.warn(e.code, e.message)}
105
+ />
106
+ ```
107
+
108
+ ### 3) 图片识别 `decodeImage`
109
+
110
+ ```tsx
111
+ import { decodeImage } from '@unif/react-native-hms-scan';
112
+
113
+ const results = await decodeImage('file:///path/photo.jpg', { formats: ['QR_CODE'] });
114
+ // results: ScanResult[](可能为空)。只吃本地 uri,远程图请先下载。
115
+ ```
116
+
117
+ ### 4) 相机权限
118
+
119
+ ```tsx
120
+ import { getCameraPermissionStatus, requestCameraPermission } from '@unif/react-native-hms-scan';
121
+ const status = await requestCameraPermission(); // 'granted' | 'denied' | 'blocked' | 'undetermined'
122
+ ```
123
+
124
+ ## 平台差异(务必知悉)
125
+
126
+ | 能力 | Android (`RemoteView`) | iOS (`HmsCustomScanViewController`) |
127
+ | --- | --- | --- |
128
+ | 手电筒 `torch` | ✅ 可编程控制 | ⚠️ HMS 无公开手电 API;本库走 `AVCaptureDevice` **尽力而为**,不保证 |
129
+ | 变焦 | ❌ 仅内部自动 | ❌ 仅内部自动 |
130
+ | 暗光提示 `onTorchStatus.available` | ✅ 上报 | ❌ 不上报 |
131
+
132
+ ## 测试(Jest)
133
+
134
+ 消费者在测试里可用内置 mock 替换本库(避免加载原生):
135
+
136
+ ```ts
137
+ jest.mock('@unif/react-native-hms-scan', () =>
138
+ require('@unif/react-native-hms-scan/mock')
139
+ );
140
+ // decodeImage / 权限方法是 jest.fn;<HmsScanView>/<Scanner> 渲染为 null。
141
+ ```
142
+
143
+ ## 类型一览
144
+
145
+ `BarcodeFormat`(QR_CODE / EAN_13 / … / UNKNOWN)、`BarcodeContentType`、`ScanResult { value, format, contentType?, cornerPoints? }`、`ScanProduct`、`CameraPermissionStatus`、`HmsScanError`。
146
+
147
+ ## 许可
148
+
149
+ MIT © unif-design
@@ -0,0 +1,30 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "ReactNativeHmsScan"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/unif-design/react-native-hms-scan.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+ # 华为 HUAWEI Scan Kit(iOS 自包含,无需 AppGallery Connect / API Key)。
20
+ # 提供 HmsCustomScanViewController(定制视图)与 HmsBitMap(图片识别)。
21
+ s.dependency "ScanKitFrameWork", "~> 1.1.2"
22
+
23
+ # ScanKitFrameWork 是 arm64-only 的静态 framework,不含模拟器 arm64 切片。
24
+ # 在 Apple 芯片上排掉模拟器 arm64(模拟器走 x86_64 / Rosetta),真机 arm64 不受影响。
25
+ s.pod_target_xcconfig = { "EXCLUDED_ARCHS[sdk=iphonesimulator*]" => "arm64" }
26
+ s.user_target_xcconfig = { "EXCLUDED_ARCHS[sdk=iphonesimulator*]" => "arm64" }
27
+
28
+ # 接入 New Architecture(Fabric 组件 + TurboModule)的 codegen 依赖。
29
+ install_modules_dependencies(s)
30
+ end
@@ -0,0 +1,79 @@
1
+ buildscript {
2
+ ext.ReactNativeHmsScan = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36,
6
+ targetSdkVersion: 36
7
+ ]
8
+
9
+ ext.getExtOrDefault = { prop ->
10
+ if (rootProject.ext.has(prop)) {
11
+ return rootProject.ext.get(prop)
12
+ }
13
+
14
+ return ReactNativeHmsScan[prop]
15
+ }
16
+
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+
22
+ dependencies {
23
+ classpath "com.android.tools.build:gradle:8.7.2"
24
+ // noinspection DifferentKotlinGradleVersion
25
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
26
+ }
27
+ }
28
+
29
+ apply plugin: "com.android.library"
30
+ apply plugin: "kotlin-android"
31
+ // "com.facebook.react" triggers RN codegen for this library (TurboModule spec +
32
+ // Fabric view manager interface/delegate) when the host app builds.
33
+ apply plugin: "com.facebook.react"
34
+
35
+ android {
36
+ namespace "com.unif.reactnativehmsscan"
37
+
38
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
39
+
40
+ defaultConfig {
41
+ minSdkVersion getExtOrDefault("minSdkVersion")
42
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
43
+ // Ship Huawei HMS keep rules to the consuming app so release builds don't strip them.
44
+ consumerProguardFiles 'consumer-rules.pro'
45
+ }
46
+
47
+ buildFeatures {
48
+ buildConfig true
49
+ }
50
+
51
+ buildTypes {
52
+ release {
53
+ minifyEnabled false
54
+ }
55
+ }
56
+
57
+ lint {
58
+ disable "GradleCompatible"
59
+ }
60
+
61
+ compileOptions {
62
+ sourceCompatibility JavaVersion.VERSION_1_8
63
+ targetCompatibility JavaVersion.VERSION_1_8
64
+ }
65
+ }
66
+
67
+ repositories {
68
+ mavenCentral()
69
+ google()
70
+ // Huawei Maven repository hosting the HMS Scan Kit (Scan SDK-Plus) artifacts.
71
+ maven { url 'https://developer.huawei.com/repo/' }
72
+ }
73
+
74
+ dependencies {
75
+ implementation "com.facebook.react:react-android"
76
+ // HUAWEI Scan Kit, Scan SDK-Plus (RemoteView + ScanUtil.decodeWithBitmap).
77
+ // No AppGallery Connect / agconnect plugin / API key required for these APIs.
78
+ implementation 'com.huawei.hms:scanplus:2.15.0.301'
79
+ }
@@ -0,0 +1,13 @@
1
+ # Keep rules for HUAWEI Scan Kit (Scan SDK-Plus). Shipped to the consuming app via
2
+ # consumerProguardFiles so release (minified) builds keep the HMS reflection targets.
3
+ # Mirrors the official hms-scan-demo proguard configuration.
4
+ -ignorewarnings
5
+ -keepattributes *Annotation*
6
+ -keepattributes Exceptions
7
+ -keepattributes InnerClasses
8
+ -keepattributes Signature
9
+ -keepattributes SourceFile,LineNumberTable
10
+
11
+ -keep class com.huawei.hianalytics.**{*;}
12
+ -keep class com.huawei.updatesdk.**{*;}
13
+ -keep class com.huawei.hms.**{*;}
@@ -0,0 +1,23 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+
3
+ <!-- Camera preview / scanning (RemoteView). -->
4
+ <uses-permission android:name="android.permission.CAMERA" />
5
+
6
+ <!-- Reading a local image for decodeImage().
7
+ Android 13+ (API 33) uses the scoped media permission; older devices fall
8
+ back to READ_EXTERNAL_STORAGE (capped at API 32 so the merger doesn't
9
+ force it on newer OS versions). -->
10
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
11
+ <uses-permission
12
+ android:name="android.permission.READ_EXTERNAL_STORAGE"
13
+ android:maxSdkVersion="32" />
14
+
15
+ <!-- Camera is used but not strictly required (gallery decode works without it). -->
16
+ <uses-feature
17
+ android:name="android.hardware.camera"
18
+ android:required="false" />
19
+ <uses-feature
20
+ android:name="android.hardware.camera.autofocus"
21
+ android:required="false" />
22
+
23
+ </manifest>
@@ -0,0 +1,192 @@
1
+ package com.unif.reactnativehmsscan
2
+
3
+ import android.Manifest
4
+ import android.app.Activity
5
+ import android.content.pm.PackageManager
6
+ import android.graphics.Bitmap
7
+ import android.graphics.BitmapFactory
8
+ import android.net.Uri
9
+ import androidx.core.app.ActivityCompat
10
+ import androidx.core.content.ContextCompat
11
+ import com.facebook.react.bridge.Promise
12
+ import com.facebook.react.bridge.ReactApplicationContext
13
+ import com.facebook.react.modules.core.PermissionAwareActivity
14
+ import com.facebook.react.modules.core.PermissionListener
15
+ import com.huawei.hms.hmsscankit.ScanUtil
16
+ import com.huawei.hms.ml.scan.HmsScan
17
+ import com.huawei.hms.ml.scan.HmsScanAnalyzerOptions
18
+ import java.io.File
19
+ import java.io.InputStream
20
+
21
+ /**
22
+ * TurboModule "HmsScan". Implements the codegen-generated [NativeHmsScanSpec]:
23
+ * - decodeImage(): decode a local image into a ScanResult[] JSON string.
24
+ * - getCameraPermissionStatus() / requestCameraPermission(): camera permission.
25
+ */
26
+ class HmsScanModule(reactContext: ReactApplicationContext) :
27
+ NativeHmsScanSpec(reactContext) {
28
+
29
+ override fun getName(): String = NAME
30
+
31
+ // ── decodeImage ──────────────────────────────────────────────────────────
32
+
33
+ /**
34
+ * Decode barcodes/QR codes from a local image. Loads the bitmap from a
35
+ * file:// / content:// URI or an absolute path, runs ScanUtil.decodeWithBitmap
36
+ * (photo mode), and resolves the ScanResult[] JSON. Never rejects on "no code
37
+ * found" — it resolves an empty array, matching the JS contract.
38
+ */
39
+ override fun decodeImage(uri: String, formatsCsv: String, promise: Promise) {
40
+ val bitmap = loadBitmap(uri)
41
+ if (bitmap == null) {
42
+ promise.reject(E_IMAGE_LOAD_FAILED, "Failed to load image from uri: $uri")
43
+ return
44
+ }
45
+
46
+ try {
47
+ val types = HmsScanResultMapper.parseFormatsCsv(formatsCsv)
48
+ val optionsCreator = HmsScanAnalyzerOptions.Creator().setPhotoMode(true)
49
+ if (types != null && types.isNotEmpty()) {
50
+ val first = types.first()
51
+ val rest = if (types.size > 1) types.copyOfRange(1, types.size) else IntArray(0)
52
+ optionsCreator.setHmsScanTypes(first, *rest)
53
+ } else {
54
+ optionsCreator.setHmsScanTypes(HmsScan.ALL_SCAN_TYPE)
55
+ }
56
+ val options = optionsCreator.create()
57
+
58
+ val scans = ScanUtil.decodeWithBitmap(reactApplicationContext, bitmap, options)
59
+ promise.resolve(HmsScanResultMapper.toJson(scans))
60
+ } catch (e: Throwable) {
61
+ promise.reject(E_DECODE_FAILED, e.message ?: "Failed to decode image", e)
62
+ } finally {
63
+ if (!bitmap.isRecycled) {
64
+ bitmap.recycle()
65
+ }
66
+ }
67
+ }
68
+
69
+ /** Resolve a uri/path to a decoded [Bitmap], or null on any failure. */
70
+ private fun loadBitmap(uri: String): Bitmap? {
71
+ return try {
72
+ val parsed = Uri.parse(uri)
73
+ val scheme = parsed.scheme?.lowercase()
74
+ when (scheme) {
75
+ "content", "file", "android.resource" -> {
76
+ val resolver = reactApplicationContext.contentResolver
77
+ var stream: InputStream? = null
78
+ try {
79
+ stream = resolver.openInputStream(parsed)
80
+ if (stream == null) null else BitmapFactory.decodeStream(stream)
81
+ } finally {
82
+ stream?.close()
83
+ }
84
+ }
85
+ null -> {
86
+ // No scheme -> treat as an absolute filesystem path.
87
+ val path = parsed.path ?: uri
88
+ decodeFile(path)
89
+ }
90
+ else -> {
91
+ // Unsupported scheme (e.g. http/https): JS contract forbids remote URLs.
92
+ null
93
+ }
94
+ }
95
+ } catch (e: Throwable) {
96
+ null
97
+ }
98
+ }
99
+
100
+ private fun decodeFile(path: String): Bitmap? {
101
+ val file = File(path)
102
+ if (!file.exists() || !file.canRead()) return null
103
+ return BitmapFactory.decodeFile(file.absolutePath)
104
+ }
105
+
106
+ // ── Camera permission ────────────────────────────────────────────────────
107
+
108
+ /**
109
+ * Current camera permission without prompting.
110
+ *
111
+ * Note: at query time Android exposes no reliable signal to distinguish
112
+ * "blocked" / "undetermined" from a plain denial (shouldShowRequestPermissionRationale
113
+ * is false for both the never-asked and permanently-denied states), so a
114
+ * not-granted result is reported as "denied". The blocked/undetermined nuance is
115
+ * resolved by requestCameraPermission() via the post-request rationale check.
116
+ */
117
+ override fun getCameraPermissionStatus(promise: Promise) {
118
+ promise.resolve(if (hasCameraPermission()) GRANTED else DENIED)
119
+ }
120
+
121
+ /**
122
+ * Request the camera permission (system dialog if needed) and resolve the
123
+ * resulting status:
124
+ * - granted: permission held.
125
+ * - denied: rejected but the app may ask again (rationale should be shown).
126
+ * - blocked: rejected with "don't ask again" (rationale will not be shown).
127
+ */
128
+ override fun requestCameraPermission(promise: Promise) {
129
+ if (hasCameraPermission()) {
130
+ promise.resolve(GRANTED)
131
+ return
132
+ }
133
+
134
+ val activity = currentActivity
135
+ if (activity == null || activity !is PermissionAwareActivity) {
136
+ // Can't surface a system dialog without a PermissionAwareActivity; report the
137
+ // current (not-granted) state rather than hanging the promise.
138
+ promise.reject(
139
+ E_NO_ACTIVITY,
140
+ "Cannot request camera permission: no current PermissionAwareActivity"
141
+ )
142
+ return
143
+ }
144
+
145
+ val permissionAwareActivity = activity as PermissionAwareActivity
146
+ val listener = PermissionListener { requestCode, _, grantResults ->
147
+ if (requestCode != CAMERA_PERMISSION_REQUEST_CODE) {
148
+ return@PermissionListener false
149
+ }
150
+ val granted = grantResults.isNotEmpty() &&
151
+ grantResults[0] == PackageManager.PERMISSION_GRANTED
152
+ val status = when {
153
+ granted -> GRANTED
154
+ // After a denial: rationale==true means the user can be asked again;
155
+ // rationale==false means "don't ask again" (blocked).
156
+ ActivityCompat.shouldShowRequestPermissionRationale(
157
+ activity,
158
+ Manifest.permission.CAMERA
159
+ ) -> DENIED
160
+ else -> BLOCKED
161
+ }
162
+ promise.resolve(status)
163
+ true
164
+ }
165
+
166
+ permissionAwareActivity.requestPermissions(
167
+ arrayOf(Manifest.permission.CAMERA),
168
+ CAMERA_PERMISSION_REQUEST_CODE,
169
+ listener
170
+ )
171
+ }
172
+
173
+ private fun hasCameraPermission(): Boolean =
174
+ ContextCompat.checkSelfPermission(
175
+ reactApplicationContext,
176
+ Manifest.permission.CAMERA
177
+ ) == PackageManager.PERMISSION_GRANTED
178
+
179
+ companion object {
180
+ const val NAME = NativeHmsScanSpec.NAME
181
+
182
+ private const val GRANTED = "granted"
183
+ private const val DENIED = "denied"
184
+ private const val BLOCKED = "blocked"
185
+
186
+ private const val E_IMAGE_LOAD_FAILED = "E_IMAGE_LOAD_FAILED"
187
+ private const val E_DECODE_FAILED = "E_DECODE_FAILED"
188
+ private const val E_NO_ACTIVITY = "E_NO_ACTIVITY"
189
+
190
+ private const val CAMERA_PERMISSION_REQUEST_CODE = 0x48_4D // "HM"
191
+ }
192
+ }
@@ -0,0 +1,145 @@
1
+ package com.unif.reactnativehmsscan
2
+
3
+ import com.huawei.hms.ml.scan.HmsScan
4
+ import org.json.JSONArray
5
+ import org.json.JSONObject
6
+
7
+ /**
8
+ * Maps HUAWEI Scan Kit [HmsScan] results to the JSON contract shared with the JS
9
+ * side (see src/format.ts / src/types.ts). Both decodeImage() and the onScanResult
10
+ * event return a JSON-encoded array of these objects.
11
+ *
12
+ * Shape:
13
+ * ```json
14
+ * [{"value":"...","format":"EAN_13","contentType":"ARTICLE",
15
+ * "cornerPoints":[{"x":10,"y":20}]}]
16
+ * ```
17
+ */
18
+ internal object HmsScanResultMapper {
19
+
20
+ /**
21
+ * Serialize an array of [HmsScan] into the ScanResult[] JSON string. The element
22
+ * type is nullable because the HMS APIs (decodeWithBitmap / OnResultCallback)
23
+ * hand back a Java array that can contain nulls.
24
+ */
25
+ fun toJson(scans: Array<out HmsScan?>?): String {
26
+ val array = JSONArray()
27
+ if (scans != null) {
28
+ for (scan in scans) {
29
+ if (scan == null) continue
30
+ val value = scan.getOriginalValue()
31
+ // A hit with no value is meaningless to the JS layer (it would be dropped
32
+ // by coerceResult anyway); skip it to keep the payload clean.
33
+ if (value.isNullOrEmpty()) continue
34
+ array.put(toJsonObject(scan, value))
35
+ }
36
+ }
37
+ return array.toString()
38
+ }
39
+
40
+ private fun toJsonObject(scan: HmsScan, value: String): JSONObject {
41
+ val obj = JSONObject()
42
+ obj.put("value", value)
43
+ obj.put("format", mapScanType(scan.getScanType()))
44
+
45
+ obj.put("contentType", mapContentType(scan.getScanTypeForm()))
46
+
47
+ val cornerPoints = scan.getCornerPoints()
48
+ if (cornerPoints != null && cornerPoints.isNotEmpty()) {
49
+ val points = JSONArray()
50
+ for (point in cornerPoints) {
51
+ if (point == null) continue
52
+ points.put(
53
+ JSONObject().apply {
54
+ put("x", point.x)
55
+ put("y", point.y)
56
+ }
57
+ )
58
+ }
59
+ if (points.length() > 0) {
60
+ obj.put("cornerPoints", points)
61
+ }
62
+ }
63
+
64
+ return obj
65
+ }
66
+
67
+ /** HmsScan.getScanType() (barcode symbology) -> unified BarcodeFormat string. */
68
+ fun mapScanType(scanType: Int): String = when (scanType) {
69
+ HmsScan.QRCODE_SCAN_TYPE -> "QR_CODE"
70
+ HmsScan.AZTEC_SCAN_TYPE -> "AZTEC"
71
+ HmsScan.DATAMATRIX_SCAN_TYPE -> "DATA_MATRIX"
72
+ HmsScan.PDF417_SCAN_TYPE -> "PDF417"
73
+ HmsScan.CODABAR_SCAN_TYPE -> "CODABAR"
74
+ HmsScan.CODE39_SCAN_TYPE -> "CODE_39"
75
+ HmsScan.CODE93_SCAN_TYPE -> "CODE_93"
76
+ HmsScan.CODE128_SCAN_TYPE -> "CODE_128"
77
+ HmsScan.EAN8_SCAN_TYPE -> "EAN_8"
78
+ HmsScan.EAN13_SCAN_TYPE -> "EAN_13"
79
+ HmsScan.UPCCODE_A_SCAN_TYPE -> "UPC_A"
80
+ HmsScan.UPCCODE_E_SCAN_TYPE -> "UPC_E"
81
+ HmsScan.ITF14_SCAN_TYPE -> "ITF14"
82
+ HmsScan.MULTI_FUNCTIONAL_SCAN_TYPE -> "MULTI_FUNCTIONAL"
83
+ else -> "UNKNOWN"
84
+ }
85
+
86
+ /**
87
+ * Resolve a comma-separated formats CSV into the int varargs for
88
+ * RemoteView.Builder.setFormat / HmsScanAnalyzerOptions.setHmsScanTypes.
89
+ *
90
+ * Returns null when the CSV is empty / yields no known formats, signalling the
91
+ * caller to fall back to [HmsScan.ALL_SCAN_TYPE].
92
+ */
93
+ fun parseFormatsCsv(formatsCsv: String?): IntArray? {
94
+ if (formatsCsv.isNullOrBlank()) return null
95
+ val types = formatsCsv
96
+ .split(",")
97
+ .map { it.trim() }
98
+ .filter { it.isNotEmpty() }
99
+ .mapNotNull { mapFormatString(it) }
100
+ .distinct()
101
+ return if (types.isEmpty()) null else types.toIntArray()
102
+ }
103
+
104
+ /** Unified BarcodeFormat string -> HmsScan.*_SCAN_TYPE (null when unknown). */
105
+ private fun mapFormatString(format: String): Int? = when (format) {
106
+ "QR_CODE" -> HmsScan.QRCODE_SCAN_TYPE
107
+ "AZTEC" -> HmsScan.AZTEC_SCAN_TYPE
108
+ "DATA_MATRIX" -> HmsScan.DATAMATRIX_SCAN_TYPE
109
+ "PDF417" -> HmsScan.PDF417_SCAN_TYPE
110
+ "CODABAR" -> HmsScan.CODABAR_SCAN_TYPE
111
+ "CODE_39" -> HmsScan.CODE39_SCAN_TYPE
112
+ "CODE_93" -> HmsScan.CODE93_SCAN_TYPE
113
+ "CODE_128" -> HmsScan.CODE128_SCAN_TYPE
114
+ "EAN_8" -> HmsScan.EAN8_SCAN_TYPE
115
+ "EAN_13" -> HmsScan.EAN13_SCAN_TYPE
116
+ "UPC_A" -> HmsScan.UPCCODE_A_SCAN_TYPE
117
+ "UPC_E" -> HmsScan.UPCCODE_E_SCAN_TYPE
118
+ "ITF14" -> HmsScan.ITF14_SCAN_TYPE
119
+ "MULTI_FUNCTIONAL" -> HmsScan.MULTI_FUNCTIONAL_SCAN_TYPE
120
+ else -> null
121
+ }
122
+
123
+ /**
124
+ * HmsScan.getScanTypeForm() (content semantic) -> unified BarcodeContentType
125
+ * string. Unrecognized / generic forms map to "OTHER".
126
+ *
127
+ * Uses the named HmsScan.*_FORM constants (public static final ints) rather than
128
+ * hard-coded numbers so the mapping stays correct across SDK versions.
129
+ */
130
+ private fun mapContentType(form: Int): String = when (form) {
131
+ HmsScan.URL_FORM -> "URL"
132
+ HmsScan.EMAIL_CONTENT_FORM -> "EMAIL"
133
+ HmsScan.TEL_PHONE_NUMBER_FORM -> "PHONE"
134
+ HmsScan.SMS_FORM -> "SMS"
135
+ HmsScan.WIFI_CONNECT_INFO_FORM -> "WIFI"
136
+ HmsScan.CONTACT_DETAIL_FORM -> "CONTACT"
137
+ HmsScan.EVENT_INFO_FORM -> "EVENT"
138
+ HmsScan.LOCATION_COORDINATE_FORM -> "LOCATION"
139
+ HmsScan.DRIVER_INFO_FORM -> "DRIVER"
140
+ HmsScan.ISBN_NUMBER_FORM -> "ISBN"
141
+ HmsScan.ARTICLE_NUMBER_FORM -> "ARTICLE"
142
+ HmsScan.PURE_TEXT_FORM -> "TEXT"
143
+ else -> "OTHER"
144
+ }
145
+ }