@unif/react-native-hms-scan 0.6.2 → 2.0.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 (156) hide show
  1. package/README.md +3 -2
  2. package/android/src/main/AndroidManifest.xml +0 -9
  3. package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanImage.kt +135 -0
  4. package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanModule.kt +163 -147
  5. package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanResultMapper.kt +17 -9
  6. package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanView.kt +57 -19
  7. package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanViewManager.kt +1 -1
  8. package/ios/HmsScanModule.mm +13 -37
  9. package/ios/HmsScanResultMapper.h +4 -3
  10. package/ios/HmsScanResultMapper.mm +31 -19
  11. package/ios/HmsScanView.h +1 -1
  12. package/ios/HmsScanView.mm +140 -25
  13. package/lib/module/HmsScanView/HmsScanView.js +66 -0
  14. package/lib/module/HmsScanView/HmsScanView.js.map +1 -0
  15. package/lib/module/HmsScanView/HmsScanView.web.js +26 -0
  16. package/lib/module/HmsScanView/HmsScanView.web.js.map +1 -0
  17. package/lib/module/HmsScanView/types.js +4 -0
  18. package/lib/module/HmsScanView/types.js.map +1 -0
  19. package/lib/module/HmsScanViewNativeComponent.ts +7 -5
  20. package/lib/module/NativeHmsScan.js +1 -1
  21. package/lib/module/NativeHmsScan.js.map +1 -1
  22. package/lib/module/ScanError.js +12 -0
  23. package/lib/module/ScanError.js.map +1 -0
  24. package/lib/module/Scanner/DeniedOverlay.js +1 -1
  25. package/lib/module/Scanner/DeniedOverlay.js.map +1 -1
  26. package/lib/module/Scanner/ResultFail.js +3 -2
  27. package/lib/module/Scanner/ResultFail.js.map +1 -1
  28. package/lib/module/Scanner/ResultFocus/ResultFocus.js +56 -0
  29. package/lib/module/Scanner/ResultFocus/ResultFocus.js.map +1 -0
  30. package/lib/module/Scanner/ResultFocus/styles.js +39 -0
  31. package/lib/module/Scanner/ResultFocus/styles.js.map +1 -0
  32. package/lib/module/Scanner/ResultFocus/types.js +4 -0
  33. package/lib/module/Scanner/ResultFocus/types.js.map +1 -0
  34. package/lib/module/Scanner/Scanner.js +11 -310
  35. package/lib/module/Scanner/Scanner.js.map +1 -1
  36. package/lib/module/Scanner/Scanner.web.js +29 -0
  37. package/lib/module/Scanner/Scanner.web.js.map +1 -0
  38. package/lib/module/Scanner/ScannerInner.js +310 -0
  39. package/lib/module/Scanner/ScannerInner.js.map +1 -0
  40. package/lib/module/Scanner/styles.js +11 -0
  41. package/lib/module/Scanner/styles.js.map +1 -0
  42. package/lib/module/Scanner/types.js +4 -0
  43. package/lib/module/Scanner/types.js.map +1 -0
  44. package/lib/module/constants.js +20 -0
  45. package/lib/module/constants.js.map +1 -0
  46. package/lib/module/decodeImage.js +29 -24
  47. package/lib/module/decodeImage.js.map +1 -1
  48. package/lib/module/decodeImage.web.js +7 -0
  49. package/lib/module/decodeImage.web.js.map +1 -0
  50. package/lib/module/errors.js +24 -0
  51. package/lib/module/errors.js.map +1 -0
  52. package/lib/module/format.js +53 -47
  53. package/lib/module/format.js.map +1 -1
  54. package/lib/module/index.js +5 -19
  55. package/lib/module/index.js.map +1 -1
  56. package/lib/module/index.web.js +8 -0
  57. package/lib/module/index.web.js.map +1 -0
  58. package/lib/module/mock.js +2 -23
  59. package/lib/module/mock.js.map +1 -1
  60. package/lib/module/permissions.js +24 -20
  61. package/lib/module/permissions.js.map +1 -1
  62. package/lib/module/permissions.web.js +10 -0
  63. package/lib/module/permissions.web.js.map +1 -0
  64. package/lib/module/types.js +1 -45
  65. package/lib/module/types.js.map +1 -1
  66. package/lib/typescript/src/HmsScanView/HmsScanView.d.ts +3 -0
  67. package/lib/typescript/src/HmsScanView/HmsScanView.d.ts.map +1 -0
  68. package/lib/typescript/src/HmsScanView/HmsScanView.web.d.ts +3 -0
  69. package/lib/typescript/src/HmsScanView/HmsScanView.web.d.ts.map +1 -0
  70. package/lib/typescript/src/HmsScanView/types.d.ts +12 -0
  71. package/lib/typescript/src/HmsScanView/types.d.ts.map +1 -0
  72. package/lib/typescript/src/HmsScanViewNativeComponent.d.ts +6 -4
  73. package/lib/typescript/src/HmsScanViewNativeComponent.d.ts.map +1 -1
  74. package/lib/typescript/src/NativeHmsScan.d.ts +3 -5
  75. package/lib/typescript/src/NativeHmsScan.d.ts.map +1 -1
  76. package/lib/typescript/src/ScanError.d.ts +7 -0
  77. package/lib/typescript/src/ScanError.d.ts.map +1 -0
  78. package/lib/typescript/src/Scanner/DeniedOverlay.d.ts.map +1 -1
  79. package/lib/typescript/src/Scanner/ResultFail.d.ts +2 -1
  80. package/lib/typescript/src/Scanner/ResultFail.d.ts.map +1 -1
  81. package/lib/typescript/src/Scanner/ResultFocus/ResultFocus.d.ts +4 -0
  82. package/lib/typescript/src/Scanner/ResultFocus/ResultFocus.d.ts.map +1 -0
  83. package/lib/typescript/src/Scanner/ResultFocus/styles.d.ts +36 -0
  84. package/lib/typescript/src/Scanner/ResultFocus/styles.d.ts.map +1 -0
  85. package/lib/typescript/src/Scanner/ResultFocus/types.d.ts +8 -0
  86. package/lib/typescript/src/Scanner/ResultFocus/types.d.ts.map +1 -0
  87. package/lib/typescript/src/Scanner/Scanner.d.ts +2 -45
  88. package/lib/typescript/src/Scanner/Scanner.d.ts.map +1 -1
  89. package/lib/typescript/src/Scanner/Scanner.web.d.ts +3 -0
  90. package/lib/typescript/src/Scanner/Scanner.web.d.ts.map +1 -0
  91. package/lib/typescript/src/Scanner/ScannerInner.d.ts +3 -0
  92. package/lib/typescript/src/Scanner/ScannerInner.d.ts.map +1 -0
  93. package/lib/typescript/src/Scanner/styles.d.ts +7 -0
  94. package/lib/typescript/src/Scanner/styles.d.ts.map +1 -0
  95. package/lib/typescript/src/Scanner/types.d.ts +20 -0
  96. package/lib/typescript/src/Scanner/types.d.ts.map +1 -0
  97. package/lib/typescript/src/constants.d.ts +8 -0
  98. package/lib/typescript/src/constants.d.ts.map +1 -0
  99. package/lib/typescript/src/decodeImage.d.ts +2 -12
  100. package/lib/typescript/src/decodeImage.d.ts.map +1 -1
  101. package/lib/typescript/src/decodeImage.web.d.ts +3 -0
  102. package/lib/typescript/src/decodeImage.web.d.ts.map +1 -0
  103. package/lib/typescript/src/errors.d.ts +6 -0
  104. package/lib/typescript/src/errors.d.ts.map +1 -0
  105. package/lib/typescript/src/format.d.ts +15 -12
  106. package/lib/typescript/src/format.d.ts.map +1 -1
  107. package/lib/typescript/src/index.d.ts +6 -7
  108. package/lib/typescript/src/index.d.ts.map +1 -1
  109. package/lib/typescript/src/index.web.d.ts +9 -0
  110. package/lib/typescript/src/index.web.d.ts.map +1 -0
  111. package/lib/typescript/src/mock.d.ts +9 -8
  112. package/lib/typescript/src/mock.d.ts.map +1 -1
  113. package/lib/typescript/src/permissions.d.ts +4 -8
  114. package/lib/typescript/src/permissions.d.ts.map +1 -1
  115. package/lib/typescript/src/permissions.web.d.ts +4 -0
  116. package/lib/typescript/src/permissions.web.d.ts.map +1 -0
  117. package/lib/typescript/src/types.d.ts +31 -56
  118. package/lib/typescript/src/types.d.ts.map +1 -1
  119. package/package.json +16 -9
  120. package/src/HmsScanView/HmsScanView.tsx +67 -0
  121. package/src/HmsScanView/HmsScanView.web.tsx +22 -0
  122. package/src/HmsScanView/types.ts +17 -0
  123. package/src/HmsScanViewNativeComponent.ts +7 -5
  124. package/src/NativeHmsScan.ts +3 -5
  125. package/src/ScanError.ts +13 -0
  126. package/src/Scanner/DeniedOverlay.tsx +4 -2
  127. package/src/Scanner/ResultFail.tsx +23 -5
  128. package/src/Scanner/ResultFocus/ResultFocus.tsx +43 -0
  129. package/src/Scanner/ResultFocus/styles.ts +25 -0
  130. package/src/Scanner/ResultFocus/types.ts +8 -0
  131. package/src/Scanner/Scanner.tsx +14 -432
  132. package/src/Scanner/Scanner.web.tsx +26 -0
  133. package/src/Scanner/ScannerInner.tsx +391 -0
  134. package/src/Scanner/styles.ts +6 -0
  135. package/src/Scanner/types.ts +36 -0
  136. package/src/constants.ts +55 -0
  137. package/src/decodeImage.ts +35 -29
  138. package/src/decodeImage.web.ts +8 -0
  139. package/src/errors.ts +26 -0
  140. package/src/format.ts +77 -76
  141. package/src/index.tsx +17 -32
  142. package/src/index.web.tsx +22 -0
  143. package/src/mock.ts +21 -29
  144. package/src/permissions.ts +33 -20
  145. package/src/permissions.web.ts +9 -0
  146. package/src/types.ts +38 -126
  147. package/lib/module/HmsScanView.js +0 -43
  148. package/lib/module/HmsScanView.js.map +0 -1
  149. package/lib/module/Scanner/ResultFocus.js +0 -171
  150. package/lib/module/Scanner/ResultFocus.js.map +0 -1
  151. package/lib/typescript/src/HmsScanView.d.ts +0 -33
  152. package/lib/typescript/src/HmsScanView.d.ts.map +0 -1
  153. package/lib/typescript/src/Scanner/ResultFocus.d.ts +0 -14
  154. package/lib/typescript/src/Scanner/ResultFocus.d.ts.map +0 -1
  155. package/src/HmsScanView.tsx +0 -87
  156. package/src/Scanner/ResultFocus.tsx +0 -122
@@ -0,0 +1,391 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { AppState, Linking, StyleSheet, View } from 'react-native';
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
+ import { HmsScanView } from '../HmsScanView/HmsScanView';
5
+ import { decodeImage } from '../decodeImage';
6
+ import {
7
+ getCameraPermissionStatus,
8
+ requestCameraPermission,
9
+ } from '../permissions';
10
+ import { formatConfiguration } from '../format';
11
+ import { scanError } from '../errors';
12
+ import type {
13
+ RequestedScanFormat,
14
+ ScannerImage,
15
+ ScanFailure,
16
+ ScanResult,
17
+ ScanTorchState,
18
+ } from '../types';
19
+ import { Viewfinder } from './Viewfinder';
20
+ import { ScanTopBar } from './ScanTopBar';
21
+ import { ScanToolbar } from './ScanToolbar';
22
+ import { ResultFocus } from './ResultFocus/ResultFocus';
23
+ import { ResultFail } from './ResultFail';
24
+ import { DeniedOverlay } from './DeniedOverlay';
25
+ import { ScanErrorOverlay } from './ScanErrorOverlay';
26
+
27
+ import { styles } from './styles';
28
+ import type { Phase, ScannerProps, SelectedResult } from './types';
29
+
30
+ export function ScannerInner({
31
+ title = '扫一扫',
32
+ hintText = '将条码 / 二维码放入框内,自动扫描',
33
+ formats,
34
+ topInset,
35
+ bottomInset,
36
+ showTorch = true,
37
+ autoConfirm = false,
38
+ pickImage,
39
+ onConfirm,
40
+ onClose,
41
+ onError,
42
+ }: ScannerProps) {
43
+ const insets = useSafeAreaInsets();
44
+ const config = formatConfiguration(formats);
45
+ const configKey = config.error?.message ?? config.csv;
46
+ const configRef = useRef(config);
47
+ configRef.current = config;
48
+ const callbacks = useRef({ onConfirm, onClose, onError, autoConfirm });
49
+ callbacks.current = { onConfirm, onClose, onError, autoConfirm };
50
+ const [phase, setPhase] = useState<Phase>('init');
51
+ const [result, setResult] = useState<Readonly<ScanResult> | null>(null);
52
+ const [torchRequest, setTorchRequest] = useState(false);
53
+ const [torchState, setTorchState] = useState<Readonly<ScanTorchState>>({
54
+ on: false,
55
+ });
56
+ const [scanSession, setScanSession] = useState(0);
57
+ const [activeFormats, setActiveFormats] = useState<
58
+ readonly RequestedScanFormat[]
59
+ >([]);
60
+ const mounted = useRef(false);
61
+ const closed = useRef(false);
62
+ const accepting = useRef(false);
63
+ const cameraSession = useRef(0);
64
+ const permissionRun = useRef(0);
65
+ const imageRun = useRef(0);
66
+ const imagePending = useRef(false);
67
+ const restartAfterImage = useRef(false);
68
+ const waitingForSettings = useRef(false);
69
+ const selected = useRef<SelectedResult | null>(null);
70
+
71
+ const stopAdoption = useCallback(() => {
72
+ accepting.current = false;
73
+ ++cameraSession.current;
74
+ ++imageRun.current;
75
+ ++permissionRun.current;
76
+ selected.current = null;
77
+ }, []);
78
+ const startScan = useCallback(() => {
79
+ if (!mounted.current || closed.current) return;
80
+ if (imagePending.current) {
81
+ restartAfterImage.current = true;
82
+ setPhase('processing');
83
+ return;
84
+ }
85
+ const current = configRef.current;
86
+ if (current.error) {
87
+ setPhase('error');
88
+ callbacks.current.onError?.(current.error);
89
+ return;
90
+ }
91
+ setActiveFormats(
92
+ current.csv ? (current.csv.split(',') as RequestedScanFormat[]) : []
93
+ );
94
+ setResult(null);
95
+ selected.current = null;
96
+ accepting.current = true;
97
+ setScanSession(++cameraSession.current);
98
+ setPhase('scan');
99
+ }, []);
100
+ const reportFatal = useCallback(
101
+ (error: Readonly<ScanFailure>) => {
102
+ stopAdoption();
103
+ setResult(null);
104
+ setTorchRequest(false);
105
+ setPhase(error.reason === 'permission_denied' ? 'denied' : 'error');
106
+ callbacks.current.onError?.(error);
107
+ },
108
+ [stopAdoption]
109
+ );
110
+ const runPermission = useCallback(
111
+ async (requestIfNeeded: boolean) => {
112
+ const run = ++permissionRun.current;
113
+ const current = () =>
114
+ mounted.current && !closed.current && run === permissionRun.current;
115
+ let status;
116
+ try {
117
+ status = await getCameraPermissionStatus();
118
+ if (!current()) return;
119
+ if (
120
+ requestIfNeeded &&
121
+ (status === 'denied' || status === 'undetermined')
122
+ ) {
123
+ status = await requestCameraPermission();
124
+ if (!current()) return;
125
+ }
126
+ } catch (error) {
127
+ if (current()) reportFatal(error as ScanFailure);
128
+ return;
129
+ }
130
+ if (status === 'granted') startScan();
131
+ else {
132
+ accepting.current = false;
133
+ setPhase('denied');
134
+ }
135
+ },
136
+ [reportFatal, startScan]
137
+ );
138
+
139
+ useEffect(() => {
140
+ mounted.current = true;
141
+ return () => {
142
+ mounted.current = false;
143
+ stopAdoption();
144
+ };
145
+ }, [stopAdoption]);
146
+ useEffect(() => {
147
+ if (closed.current) return;
148
+ stopAdoption();
149
+ setResult(null);
150
+ if (imagePending.current) {
151
+ restartAfterImage.current = true;
152
+ setPhase('processing');
153
+ return;
154
+ }
155
+ if (configRef.current.error) {
156
+ reportFatal(configRef.current.error);
157
+ return;
158
+ }
159
+ setPhase('init');
160
+ void runPermission(true);
161
+ }, [configKey, reportFatal, runPermission, stopAdoption]);
162
+ useEffect(() => {
163
+ const subscription = AppState.addEventListener('change', (state) => {
164
+ if (state === 'active' && waitingForSettings.current && !closed.current) {
165
+ waitingForSettings.current = false;
166
+ void runPermission(false);
167
+ }
168
+ });
169
+ return () => subscription.remove();
170
+ }, [runPermission]);
171
+ useEffect(() => {
172
+ if (!showTorch) setTorchRequest(false);
173
+ }, [showTorch]);
174
+
175
+ const deliver = useCallback(() => {
176
+ const item = selected.current;
177
+ if (!mounted.current || closed.current || !item || item.delivered) return;
178
+ // Mark delivery before calling business code. A callback failure cannot undo it.
179
+ item.delivered = true;
180
+ setPhase('done');
181
+ callbacks.current.onConfirm(item.result);
182
+ }, []);
183
+ const adopt = useCallback(
184
+ (next: Readonly<ScanResult>) => {
185
+ selected.current = { result: next, delivered: false };
186
+ setResult(next);
187
+ setPhase('result');
188
+ if (callbacks.current.autoConfirm) deliver();
189
+ },
190
+ [deliver]
191
+ );
192
+ const onCameraResult = useCallback(
193
+ (results: readonly ScanResult[]) => {
194
+ if (
195
+ !mounted.current ||
196
+ closed.current ||
197
+ !accepting.current ||
198
+ scanSession !== cameraSession.current
199
+ )
200
+ return;
201
+ const first = results[0];
202
+ if (!first) return;
203
+ accepting.current = false;
204
+ adopt(first);
205
+ },
206
+ [adopt, scanSession]
207
+ );
208
+ const onCameraError = useCallback(
209
+ (error: Readonly<ScanFailure>) => {
210
+ if (
211
+ !mounted.current ||
212
+ closed.current ||
213
+ !accepting.current ||
214
+ scanSession !== cameraSession.current
215
+ )
216
+ return;
217
+ reportFatal(error);
218
+ },
219
+ [reportFatal, scanSession]
220
+ );
221
+ const close = useCallback(() => {
222
+ if (closed.current) return;
223
+ closed.current = true;
224
+ stopAdoption();
225
+ setTorchRequest(false);
226
+ setPhase('closed');
227
+ callbacks.current.onClose?.();
228
+ }, [stopAdoption]);
229
+ const reset = useCallback(() => {
230
+ stopAdoption();
231
+ startScan();
232
+ }, [startScan, stopAdoption]);
233
+ const retry = useCallback(() => {
234
+ stopAdoption();
235
+ if (imagePending.current) {
236
+ restartAfterImage.current = true;
237
+ return;
238
+ }
239
+ setPhase('init');
240
+ void runPermission(true);
241
+ }, [runPermission, stopAdoption]);
242
+ const openSettings = useCallback(async () => {
243
+ waitingForSettings.current = true;
244
+ try {
245
+ await Linking.openSettings();
246
+ } catch (error) {
247
+ waitingForSettings.current = false;
248
+ if (mounted.current && !closed.current)
249
+ reportFatal(
250
+ scanError(error, {
251
+ reason: 'unavailable',
252
+ message: '打开系统设置失败',
253
+ })
254
+ );
255
+ }
256
+ }, [reportFatal]);
257
+
258
+ const onAlbum = useCallback(async () => {
259
+ if (
260
+ !pickImage ||
261
+ !accepting.current ||
262
+ imagePending.current ||
263
+ closed.current
264
+ )
265
+ return;
266
+ accepting.current = false;
267
+ const run = ++imageRun.current;
268
+ const current = () =>
269
+ mounted.current && !closed.current && run === imageRun.current;
270
+ const inputFormats = activeFormats;
271
+ imagePending.current = true;
272
+ setPhase('processing');
273
+ let image: ScannerImage | null = null;
274
+ let results: readonly ScanResult[] | undefined;
275
+ let failure: ScanFailure | undefined;
276
+ try {
277
+ image = await pickImage();
278
+ if (current() && image)
279
+ results = await decodeImage({ uri: image.uri, formats: inputFormats });
280
+ } catch (error) {
281
+ failure = scanError(error, {
282
+ reason: 'image_unavailable',
283
+ message: '选择或读取图片失败',
284
+ });
285
+ } finally {
286
+ imagePending.current = false;
287
+ // The original provider owns cleanup, including after this component unmounts.
288
+ try {
289
+ image?.onSourceReleased?.();
290
+ } finally {
291
+ if (current()) {
292
+ if (failure) {
293
+ setPhase('imageError');
294
+ callbacks.current.onError?.(failure);
295
+ } else if (!image) startScan();
296
+ else if (results?.[0]) adopt(results[0]);
297
+ else setPhase('empty');
298
+ } else if (
299
+ restartAfterImage.current &&
300
+ mounted.current &&
301
+ !closed.current
302
+ ) {
303
+ restartAfterImage.current = false;
304
+ setPhase('init');
305
+ void runPermission(true);
306
+ }
307
+ }
308
+ }
309
+ }, [activeFormats, adopt, pickImage, runPermission, startScan]);
310
+
311
+ const hasCamera = !['init', 'denied', 'error', 'closed'].includes(phase);
312
+ const showChrome = phase === 'scan' || phase === 'processing';
313
+ const bottom = bottomInset ?? insets.bottom;
314
+ return (
315
+ <View style={styles.root}>
316
+ {hasCamera && (
317
+ <HmsScanView
318
+ key={scanSession}
319
+ style={StyleSheet.absoluteFill}
320
+ formats={activeFormats}
321
+ paused={phase !== 'scan'}
322
+ torch={torchRequest}
323
+ onScanResult={onCameraResult}
324
+ onScanError={onCameraError}
325
+ onTorchState={(state) => {
326
+ if (
327
+ !closed.current &&
328
+ mounted.current &&
329
+ scanSession === cameraSession.current
330
+ ) {
331
+ setTorchState(state);
332
+ // A failed request or the SDK's controls can change actual state.
333
+ // Keep the next toggle based on that report, without optimistic UI.
334
+ setTorchRequest(showTorch && state.on);
335
+ }
336
+ }}
337
+ />
338
+ )}
339
+ {showChrome && (
340
+ <Viewfinder
341
+ size={256}
342
+ detecting={phase === 'processing'}
343
+ hintText={hintText}
344
+ />
345
+ )}
346
+ {hasCamera && (
347
+ <ScanTopBar title={title} topInset={topInset ?? insets.top} />
348
+ )}
349
+ {showChrome && (
350
+ <ScanToolbar
351
+ flash={torchState.on}
352
+ bottomInset={bottom}
353
+ onFlash={
354
+ showTorch && torchState.available !== false
355
+ ? () => setTorchRequest((value) => !value)
356
+ : undefined
357
+ }
358
+ onAlbum={pickImage ? onAlbum : undefined}
359
+ onClose={onClose ? close : undefined}
360
+ />
361
+ )}
362
+ {phase === 'result' && result && (
363
+ <ResultFocus
364
+ result={result}
365
+ bottomInset={bottom}
366
+ onRescan={reset}
367
+ onConfirm={deliver}
368
+ />
369
+ )}
370
+ {(phase === 'empty' || phase === 'imageError') && (
371
+ <ResultFail
372
+ bottomInset={bottom}
373
+ failed={phase === 'imageError'}
374
+ onRescan={reset}
375
+ />
376
+ )}
377
+ {phase === 'error' && (
378
+ <ScanErrorOverlay
379
+ onClose={onClose ? close : undefined}
380
+ onRetry={retry}
381
+ />
382
+ )}
383
+ {phase === 'denied' && (
384
+ <DeniedOverlay
385
+ onClose={onClose ? close : undefined}
386
+ onSettings={openSettings}
387
+ />
388
+ )}
389
+ </View>
390
+ );
391
+ }
@@ -0,0 +1,6 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { scanChrome } from './scanChrome';
3
+
4
+ export const styles = StyleSheet.create({
5
+ root: { flex: 1, backgroundColor: scanChrome.cameraBg },
6
+ });
@@ -0,0 +1,36 @@
1
+ import type {
2
+ RequestedScanFormat,
3
+ ScannerImage,
4
+ ScanFailure,
5
+ ScanResult,
6
+ } from '../types';
7
+
8
+ export type Phase =
9
+ | 'init'
10
+ | 'scan'
11
+ | 'processing'
12
+ | 'result'
13
+ | 'empty'
14
+ | 'imageError'
15
+ | 'denied'
16
+ | 'error'
17
+ | 'done'
18
+ | 'closed';
19
+ export interface ScannerProps {
20
+ title?: string;
21
+ hintText?: string;
22
+ formats?: readonly RequestedScanFormat[];
23
+ topInset?: number;
24
+ bottomInset?: number;
25
+ showTorch?: boolean;
26
+ autoConfirm?: boolean;
27
+ pickImage?(): Promise<ScannerImage | null>;
28
+ onConfirm(result: Readonly<ScanResult>): void;
29
+ onClose?(): void;
30
+ onError?(error: Readonly<ScanFailure>): void;
31
+ }
32
+
33
+ export interface SelectedResult {
34
+ result: Readonly<ScanResult>;
35
+ delivered: boolean;
36
+ }
@@ -0,0 +1,55 @@
1
+ import type { ScanFailure } from './types';
2
+
3
+ /** The one source for public scan formats and input validation. */
4
+ export const SCAN_FORMATS = [
5
+ 'QR_CODE',
6
+ 'AZTEC',
7
+ 'DATA_MATRIX',
8
+ 'PDF417',
9
+ 'CODABAR',
10
+ 'CODE_39',
11
+ 'CODE_93',
12
+ 'CODE_128',
13
+ 'EAN_8',
14
+ 'EAN_13',
15
+ 'UPC_A',
16
+ 'UPC_E',
17
+ 'ITF14',
18
+ 'MULTI_FUNCTIONAL',
19
+ 'UNKNOWN',
20
+ ] as const;
21
+ export const SCAN_CONTENT_TYPES = [
22
+ 'TEXT',
23
+ 'URL',
24
+ 'EMAIL',
25
+ 'PHONE',
26
+ 'SMS',
27
+ 'WIFI',
28
+ 'CONTACT',
29
+ 'EVENT',
30
+ 'LOCATION',
31
+ 'DRIVER',
32
+ 'ISBN',
33
+ 'ARTICLE',
34
+ 'OTHER',
35
+ ] as const;
36
+
37
+ export const SCAN_FORMAT_SET: ReadonlySet<string> = new Set(SCAN_FORMATS);
38
+ export const SCAN_CONTENT_TYPE_SET: ReadonlySet<string> = new Set(
39
+ SCAN_CONTENT_TYPES
40
+ );
41
+
42
+ export const SCAN_ERROR_REASONS: Readonly<
43
+ Record<string, ScanFailure['reason']>
44
+ > = {
45
+ E_INVALID_INPUT: 'invalid_input',
46
+ E_NO_CAMERA_PERMISSION: 'permission_denied',
47
+ E_NO_READ_PERMISSION: 'permission_denied',
48
+ E_IMAGE_LOAD_FAILED: 'image_unavailable',
49
+ E_DECODE_FAILED: 'decode_failed',
50
+ E_INVALID_RESPONSE: 'invalid_response',
51
+ E_UNSUPPORTED: 'unsupported',
52
+ E_UNSUPPORTED_FORMAT: 'unsupported',
53
+ E_CAMERA_INIT: 'unavailable',
54
+ E_UNAVAILABLE: 'unavailable',
55
+ };
@@ -1,37 +1,43 @@
1
1
  import NativeHmsScan from './NativeHmsScan';
2
2
  import { formatsToCsv, parseResultsJson } from './format';
3
- import { HmsScanError, type DecodeImageOptions, type ScanResult } from './types';
3
+ import { scanError } from './errors';
4
+ import { ScanError } from './ScanError';
5
+ import type { DecodeScanImageInput, ScanResult } from './types';
4
6
 
5
- /**
6
- * 从本地图片解码条码 / 二维码(华为 Bitmap 模式)。
7
- *
8
- * @param uri 本地图片。iOS 支持 file://、绝对路径、data:;Android 支持
9
- * file://、绝对路径、content://、android.resource://。
10
- * 两端都不下载远程 URL,也不支持 ph:// / assets-library://。
11
- * @param options 可选;formats 限定码制,不传 = 全部。
12
- * @returns 命中的结果数组(可能为空)。
13
- * @throws HmsScanError 图片加载失败 / 读权限缺失等。
14
- */
15
7
  export async function decodeImage(
16
- uri: string,
17
- options?: DecodeImageOptions
18
- ): Promise<ScanResult[]> {
19
- if (!uri) {
20
- throw new HmsScanError('E_IMAGE_LOAD_FAILED', 'decodeImage: uri 不能为空');
8
+ input: Readonly<DecodeScanImageInput>
9
+ ): Promise<readonly ScanResult[]> {
10
+ // No remote authority, empty filename, fragment, query or malformed escapes.
11
+ const uri = input?.uri;
12
+ if (
13
+ typeof uri !== 'string' ||
14
+ !/^file:\/\/\/(?:[^?#\s]+\/)*[^/?#\s]+$/i.test(uri)
15
+ ) {
16
+ throw new ScanError({
17
+ reason: 'invalid_input',
18
+ message: 'decodeImage 需要可读的 file URI',
19
+ });
21
20
  }
22
21
  try {
23
- const json = await NativeHmsScan.decodeImage(uri, formatsToCsv(options?.formats));
24
- return parseResultsJson(json);
25
- } catch (e) {
26
- // 原生 reject 的 code 透传为 HmsScanError
27
- const err = e as { code?: string; message?: string };
28
- if (
29
- err.code === 'E_NO_READ_PERMISSION' ||
30
- err.code === 'E_IMAGE_LOAD_FAILED' ||
31
- err.code === 'E_DECODE_FAILED'
32
- ) {
33
- throw new HmsScanError(err.code, err.message);
34
- }
35
- throw new HmsScanError('E_UNKNOWN', err.message ?? 'decodeImage failed');
22
+ decodeURIComponent(uri);
23
+ } catch {
24
+ throw new ScanError({
25
+ reason: 'invalid_input',
26
+ message: '图片 URI 编码无效',
27
+ });
28
+ }
29
+ const csv = formatsToCsv(input.formats);
30
+ if (!NativeHmsScan)
31
+ throw new ScanError({
32
+ reason: 'unavailable',
33
+ message: 'HMS Scan 原生模块未安装',
34
+ });
35
+ try {
36
+ return parseResultsJson(await NativeHmsScan.decodeImage(uri, csv));
37
+ } catch (error) {
38
+ throw scanError(error, {
39
+ reason: 'decode_failed',
40
+ message: '图片识别失败',
41
+ });
36
42
  }
37
43
  }
@@ -0,0 +1,8 @@
1
+ import { unsupportedScanError } from './errors';
2
+ import type { DecodeScanImageInput, ScanResult } from './types';
3
+
4
+ export async function decodeImage(
5
+ _input: Readonly<DecodeScanImageInput>
6
+ ): Promise<readonly ScanResult[]> {
7
+ throw unsupportedScanError();
8
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { ScanError } from './ScanError';
2
+ import { SCAN_ERROR_REASONS } from './constants';
3
+ import type { NativeScanFailure, ScanFailure } from './types';
4
+
5
+ /** Native boundary normalization; Scanner only uses this for its external picker. */
6
+ export function scanError(
7
+ error: unknown,
8
+ fallback: Readonly<ScanFailure>
9
+ ): ScanError {
10
+ if (error instanceof ScanError) return error;
11
+ const raw =
12
+ error && typeof error === 'object' ? (error as NativeScanFailure) : {};
13
+ const sourceCode = typeof raw.code === 'string' ? raw.code : undefined;
14
+ return new ScanError({
15
+ reason: (sourceCode && SCAN_ERROR_REASONS[sourceCode]) || fallback.reason,
16
+ message: typeof raw.message === 'string' ? raw.message : fallback.message,
17
+ ...(sourceCode ? { sourceCode } : {}),
18
+ });
19
+ }
20
+
21
+ export function unsupportedScanError(): ScanError {
22
+ return new ScanError({
23
+ reason: 'unsupported',
24
+ message: 'HMS Scan 仅支持 Android 与 iOS 设备',
25
+ });
26
+ }