@unif/react-native-hms-scan 0.6.2 → 1.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 +8 -1
  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,43 @@
1
+ import { ScrollView, Text, View } from 'react-native';
2
+ import {
3
+ Button,
4
+ Card,
5
+ useColors,
6
+ useThemedStyles,
7
+ r,
8
+ } from '@unif/react-native-design';
9
+ import type { ResultFocusProps } from './types';
10
+ import { styles, makeStyles } from './styles';
11
+
12
+ /** A raw result is selected here; interpreting it belongs to the consumer. */
13
+ export function ResultFocus({
14
+ result,
15
+ bottomInset,
16
+ onRescan,
17
+ onConfirm,
18
+ }: ResultFocusProps) {
19
+ const colors = useColors();
20
+ const themed = useThemedStyles(makeStyles);
21
+ return (
22
+ <View style={[styles.wrap, { bottom: bottomInset + r(18) }]}>
23
+ <Card
24
+ borderColor={colors.primary}
25
+ borderWidth={2}
26
+ borderRadius={r(16)}
27
+ padding={r(14)}
28
+ >
29
+ <Text style={themed.badge}>已识别</Text>
30
+ <Text style={themed.format}>{result.format}</Text>
31
+ <ScrollView style={styles.content}>
32
+ <Text selectable style={themed.value}>
33
+ {result.value}
34
+ </Text>
35
+ </ScrollView>
36
+ <View style={styles.actions}>
37
+ <Button label="重扫" variant="secondary" block onPress={onRescan} />
38
+ <Button label="选用" variant="primary" block onPress={onConfirm} />
39
+ </View>
40
+ </Card>
41
+ </View>
42
+ );
43
+ }
@@ -0,0 +1,25 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import {
3
+ r,
4
+ rf,
5
+ fw,
6
+ fontMono,
7
+ type ColorTokens,
8
+ } from '@unif/react-native-design';
9
+
10
+ export const styles = StyleSheet.create({
11
+ wrap: { position: 'absolute', left: r(16), right: r(16), zIndex: 50 },
12
+ content: { maxHeight: r(220), marginVertical: r(14) },
13
+ actions: { flexDirection: 'row', columnGap: r(10) },
14
+ });
15
+ export const makeStyles = (colors: ColorTokens) =>
16
+ StyleSheet.create({
17
+ badge: { fontSize: rf(13), fontWeight: fw.semi, color: colors.foreground },
18
+ format: {
19
+ fontSize: rf(12),
20
+ color: colors.foregroundMuted,
21
+ fontFamily: fontMono,
22
+ marginTop: r(4),
23
+ },
24
+ value: { fontSize: rf(16), lineHeight: rf(21), color: colors.foreground },
25
+ });
@@ -0,0 +1,8 @@
1
+ import type { ScanResult } from '../../types';
2
+
3
+ export interface ResultFocusProps {
4
+ result: Readonly<ScanResult>;
5
+ bottomInset: number;
6
+ onRescan(): void;
7
+ onConfirm(): void;
8
+ }
@@ -1,437 +1,19 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
2
- import { AppState, Linking, StyleSheet, View } from 'react-native';
3
- import { HmsScanView } from '../HmsScanView';
4
- import { decodeImage } from '../decodeImage';
1
+ import { useContext } from 'react';
5
2
  import {
6
- getCameraPermissionStatus,
7
- requestCameraPermission,
8
- } from '../permissions';
9
- import type {
10
- BarcodeFormat,
11
- ScanError,
12
- ScanProduct,
13
- ScanResult,
14
- } from '../types';
15
- import { scanChrome } from './scanChrome';
16
- import { Viewfinder } from './Viewfinder';
17
- import { ScanTopBar } from './ScanTopBar';
18
- import { ScanToolbar } from './ScanToolbar';
19
- import { ResultFocus } from './ResultFocus';
20
- import { ResultFail } from './ResultFail';
21
- import { DeniedOverlay } from './DeniedOverlay';
22
- import { ScanErrorOverlay } from './ScanErrorOverlay';
3
+ SafeAreaInsetsContext,
4
+ SafeAreaProvider,
5
+ } from 'react-native-safe-area-context';
6
+ import { ScannerInner } from './ScannerInner';
7
+ import type { ScannerProps } from './types';
23
8
 
24
- // 'done':autoConfirm 自动回调后的终态——相机暂停、不出卡片、不自动重扫(宿主通常已导航离开)。
25
- type Phase =
26
- | 'init'
27
- | 'scan'
28
- | 'detecting'
29
- | 'success'
30
- | 'fail'
31
- | 'denied'
32
- | 'error'
33
- | 'done';
34
-
35
- const VF_SIZE = 256;
36
- const DEFAULT_HINT = '将条码 / 二维码放入框内,自动扫描';
37
-
38
- function toScanError(error: unknown, fallbackMessage: string): ScanError {
39
- if (error && typeof error === 'object') {
40
- const native = error as { code?: unknown; message?: unknown };
41
- return {
42
- code: typeof native.code === 'string' ? native.code : 'E_UNKNOWN',
43
- message:
44
- typeof native.message === 'string' ? native.message : fallbackMessage,
45
- };
46
- }
47
- return { code: 'E_UNKNOWN', message: fallbackMessage };
48
- }
49
-
50
- export interface ScannerProps {
51
- /** 顶栏标题,默认 "扫一扫"。 */
52
- title?: string;
53
- /** 限定识别码制;不传 = 全部。 */
54
- formats?: readonly BarcodeFormat[];
55
- /** 取景态提示文案。 */
56
- hintText?: string;
57
- /** 顶部安全区(默认 54)。用 react-native-safe-area-context 时可传 insets.top。 */
58
- topInset?: number;
59
- /** 底部安全区(默认 34)。 */
60
- bottomInset?: number;
61
- /** 是否显示手电筒按钮,默认 true。手电由库内自管(Android 可用;iOS 为 best-effort,可在 iOS 上关掉)。 */
62
- showTorch?: boolean;
63
- /** 返回回调(退出扫码页,回到上一级);按钮在底部工具栏,与手电筒并排。 */
64
- onClose?: () => void;
65
- /** 扫码相机或权限检查出错时上报。 */
66
- onScanError?: (error: ScanError) => void;
67
- /**
68
- * 扫到条码后由宿主解析商品信息(用于浮层确认卡)。
69
- * 返回 null/undefined 视为"未识别",抛错亦然。不传则默认用扫到的原文当商品名。
70
- */
71
- resolveProduct?: (
72
- result: ScanResult
73
- ) => ScanProduct | null | undefined | Promise<ScanProduct | null | undefined>;
74
- /** 用户点"确认":把结果带回上一级(宿主通常在此导航返回)。 */
75
- onConfirm?: (product: ScanProduct, result: ScanResult) => void;
76
- /**
77
- * 自动确认:传 `onConfirm` 时,扫到并解析成功后**不显示结果卡**,直接触发
78
- * `onConfirm(product, result)`;未传回调时仍显示结果卡。默认 `false`(显示结果卡,
79
- * 由用户点"确定")。
80
- * 适合"扫到即用、不需要二次确认"的场景。回调后相机暂停、不自动重扫——宿主通常在
81
- * `onConfirm` 里导航离开;若需再扫由宿主控制(如重新挂载 `<Scanner>`)。
82
- * 注:未识别(`resolveProduct` 返回 `null`/抛错)仍走失败态可重扫,不会误触发。
83
- */
84
- autoConfirm?: boolean;
85
- /**
86
- * 点"相册":宿主用自己的图片选择器选图并返回本地 uri(取消则返回 null)。
87
- * 库不内置图片选择器(遵循 RN 惯例):**传了才显示相册按钮**,不传则隐藏。
88
- */
89
- pickImage?: () => Promise<string | null>;
90
- }
91
-
92
- /**
93
- * 成品「扫一扫」界面(聚焦款)。底层 <HmsScanView> 出相机画面,取景框 / 工具栏 / 结果卡
94
- * 全用 @unif/react-native-design 的主题令牌与组件绘制(统一风格),继承宿主主题与字号。
95
- * 无外层 ThemeProvider 时采用 Design 默认主题。
96
- */
9
+ /** Uses the current window's SafeAreaProvider, or measures a local one. */
97
10
  export function Scanner(props: ScannerProps) {
98
- return <ScannerInner {...props} />;
99
- }
100
-
101
- function ScannerInner({
102
- title = '扫一扫',
103
- formats,
104
- hintText = DEFAULT_HINT,
105
- topInset = 54,
106
- bottomInset = 34,
107
- showTorch = true,
108
- onClose,
109
- resolveProduct,
110
- onConfirm,
111
- autoConfirm = false,
112
- pickImage,
113
- onScanError,
114
- }: ScannerProps) {
115
- const [phase, setPhase] = useState<Phase>('init');
116
- const [torch, setTorch] = useState(false);
117
- const [product, setProduct] = useState<ScanProduct | null>(null);
118
- const [detectMs, setDetectMs] = useState(0);
119
- const [scanSession, setScanSession] = useState(0);
120
-
121
- const handlingRef = useRef(false);
122
- const acceptingResultsRef = useRef(false);
123
- const scanSessionRef = useRef(0);
124
- const detectStartRef = useRef(0);
125
- const lastResultRef = useRef<ScanResult | null>(null);
126
- const mountedRef = useRef(true);
127
- const permissionRunRef = useRef(0);
128
- const processingRunRef = useRef(0);
129
- const waitingForSettingsRef = useRef(false);
130
- const onScanErrorRef = useRef(onScanError);
131
- onScanErrorRef.current = onScanError;
132
-
133
- const invalidateProcessing = useCallback(() => {
134
- ++processingRunRef.current;
135
- }, []);
136
-
137
- const invalidateScanSession = useCallback(() => {
138
- ++scanSessionRef.current;
139
- acceptingResultsRef.current = false;
140
- }, []);
141
-
142
- const enterScan = useCallback(() => {
143
- invalidateProcessing();
144
- handlingRef.current = false;
145
- const sessionId = ++scanSessionRef.current;
146
- acceptingResultsRef.current = true;
147
- setScanSession(sessionId);
148
- setProduct(null);
149
- lastResultRef.current = null;
150
- setPhase('scan');
151
- }, [invalidateProcessing]);
152
-
153
- const enterDenied = useCallback(() => {
154
- ++permissionRunRef.current;
155
- invalidateProcessing();
156
- invalidateScanSession();
157
- setProduct(null);
158
- setPhase('denied');
159
- }, [invalidateProcessing, invalidateScanSession]);
160
-
161
- const reportFatalError = useCallback(
162
- (error: ScanError) => {
163
- // fatal 后不允许已在途的权限检查或扫码 / 相册处理链覆盖 error phase。
164
- ++permissionRunRef.current;
165
- invalidateProcessing();
166
- invalidateScanSession();
167
- setProduct(null);
168
- setPhase('error');
169
- onScanErrorRef.current?.(error);
170
- },
171
- [invalidateProcessing, invalidateScanSession]
172
- );
173
-
174
- const runPermissionFlow = useCallback(
175
- async (requestIfNeeded: boolean) => {
176
- const runId = ++permissionRunRef.current;
177
- const isCurrent = () =>
178
- mountedRef.current && permissionRunRef.current === runId;
179
- try {
180
- let status = await getCameraPermissionStatus();
181
- if (!isCurrent()) return;
182
- if (
183
- requestIfNeeded &&
184
- (status === 'undetermined' || status === 'denied')
185
- ) {
186
- status = await requestCameraPermission();
187
- if (!isCurrent()) return;
188
- }
189
- if (status === 'granted') {
190
- enterScan();
191
- } else {
192
- enterDenied();
193
- }
194
- } catch (error) {
195
- if (!isCurrent()) return;
196
- reportFatalError(toScanError(error, '检查相机权限失败'));
197
- }
198
- },
199
- [enterDenied, enterScan, reportFatalError]
200
- );
201
-
202
- // 权限流:已授权 → scan;否则请求;仍未授权 → denied。
203
- useEffect(() => {
204
- mountedRef.current = true;
205
- void runPermissionFlow(true);
206
- return () => {
207
- mountedRef.current = false;
208
- ++permissionRunRef.current;
209
- invalidateProcessing();
210
- invalidateScanSession();
211
- };
212
- }, [invalidateProcessing, invalidateScanSession, runPermissionFlow]);
213
-
214
- useEffect(() => {
215
- const subscription = AppState.addEventListener('change', (nextState) => {
216
- if (nextState === 'active' && waitingForSettingsRef.current) {
217
- waitingForSettingsRef.current = false;
218
- void runPermissionFlow(false);
219
- }
220
- });
221
- return () => subscription.remove();
222
- }, [runPermissionFlow]);
223
-
224
- useEffect(() => {
225
- if (!showTorch) setTorch(false);
226
- }, [showTorch]);
227
-
228
- const reset = useCallback(() => {
229
- enterScan();
230
- }, [enterScan]);
231
-
232
- const finalize = useCallback(
233
- async (result: ScanResult, runId: number) => {
234
- const isCurrent = () =>
235
- mountedRef.current && processingRunRef.current === runId;
236
- if (!isCurrent()) return;
237
- lastResultRef.current = result;
238
- try {
239
- const resolved = resolveProduct
240
- ? await resolveProduct(result)
241
- : { name: result.value };
242
- if (!isCurrent()) return;
243
- if (!resolved) {
244
- setPhase('fail');
245
- return;
246
- }
247
- const p: ScanProduct = {
248
- ...resolved,
249
- barcode: resolved.barcode ?? result.value,
250
- };
251
- if (autoConfirm && onConfirm) {
252
- // 跳过结果卡:直接回调,进 'done' 终态(相机暂停、不自动重扫)。
253
- onConfirm(p, result);
254
- if (!isCurrent()) return;
255
- setPhase('done');
256
- return;
257
- }
258
- setProduct(p);
259
- setDetectMs(Date.now() - detectStartRef.current);
260
- setPhase('success');
261
- } catch {
262
- if (isCurrent()) setPhase('fail');
263
- }
264
- },
265
- [resolveProduct, autoConfirm, onConfirm]
266
- );
267
-
268
- const onCameraResult = useCallback(
269
- (results: ScanResult[]) => {
270
- if (
271
- scanSession !== scanSessionRef.current ||
272
- phase !== 'scan' ||
273
- !acceptingResultsRef.current ||
274
- handlingRef.current
275
- ) {
276
- return;
277
- }
278
- const first = results[0];
279
- if (!first) return;
280
- acceptingResultsRef.current = false;
281
- handlingRef.current = true;
282
- const runId = ++processingRunRef.current;
283
- detectStartRef.current = Date.now();
284
- setPhase('detecting');
285
- void finalize(first, runId);
286
- },
287
- [phase, scanSession, finalize]
288
- );
289
-
290
- const handleViewScanError = useCallback(
291
- (error: ScanError) => {
292
- if (error.code === 'E_NO_CAMERA_PERMISSION') {
293
- enterDenied();
294
- } else if (error.code !== 'E_NO_RESULT') {
295
- reportFatalError(error);
296
- return;
297
- }
298
- onScanErrorRef.current?.(error);
299
- },
300
- [enterDenied, reportFatalError]
301
- );
302
-
303
- const onTorchStatus = useCallback(
304
- (status: { available: boolean; on: boolean }) => {
305
- setTorch(status.on);
306
- },
307
- []
308
- );
309
-
310
- const openSettings = useCallback(async () => {
311
- waitingForSettingsRef.current = true;
312
- try {
313
- await Linking.openSettings();
314
- } catch (error) {
315
- waitingForSettingsRef.current = false;
316
- if (mountedRef.current) {
317
- reportFatalError(toScanError(error, '打开系统设置失败'));
318
- }
319
- }
320
- }, [reportFatalError]);
321
-
322
- const retryCamera = useCallback(() => {
323
- invalidateProcessing();
324
- invalidateScanSession();
325
- handlingRef.current = false;
326
- setProduct(null);
327
- lastResultRef.current = null;
328
- setPhase('init');
329
- void runPermissionFlow(true);
330
- }, [invalidateProcessing, invalidateScanSession, runPermissionFlow]);
331
-
332
- const onAlbum = useCallback(async () => {
333
- if (!pickImage || !acceptingResultsRef.current || handlingRef.current) {
334
- return;
335
- }
336
- acceptingResultsRef.current = false;
337
- handlingRef.current = true;
338
- const runId = ++processingRunRef.current;
339
- const isCurrent = () =>
340
- mountedRef.current && processingRunRef.current === runId;
341
- setPhase('detecting');
342
- try {
343
- const uri = await pickImage();
344
- if (!isCurrent()) return;
345
- if (!uri) {
346
- reset();
347
- return;
348
- }
349
- detectStartRef.current = Date.now();
350
- const results = await decodeImage(uri, formats ? { formats } : undefined);
351
- if (!isCurrent()) return;
352
- const first = results[0];
353
- if (!first) {
354
- setPhase('fail');
355
- return;
356
- }
357
- await finalize(first, runId);
358
- } catch {
359
- if (isCurrent()) setPhase('fail');
360
- }
361
- }, [pickImage, formats, finalize, reset]);
362
-
363
- const onConfirmPress = useCallback(() => {
364
- const r = lastResultRef.current;
365
- const p = product;
366
- if (p && r) onConfirm?.(p, r);
367
- // 不在此 toast:确认页已展示结果,且 toast 是宿主职责(onConfirm 回调里宿主自己提示)——
368
- // 避免重复 toast,也避免宿主导航离开后依赖 Scanner 自带 ToastHost 存活的隐患。
369
- reset();
370
- }, [product, onConfirm, reset]);
371
-
372
- const showChrome = phase === 'scan' || phase === 'detecting';
373
-
374
- return (
375
- <View style={styles.root}>
376
- {phase !== 'denied' && phase !== 'init' && phase !== 'error' && (
377
- <HmsScanView
378
- style={StyleSheet.absoluteFill}
379
- formats={formats}
380
- paused={phase !== 'scan'}
381
- torch={torch}
382
- onScanResult={onCameraResult}
383
- onScanError={handleViewScanError}
384
- onTorchStatus={onTorchStatus}
385
- />
386
- )}
387
-
388
- {showChrome && (
389
- <Viewfinder
390
- size={VF_SIZE}
391
- detecting={phase === 'detecting'}
392
- hintText={hintText}
393
- />
394
- )}
395
-
396
- {phase !== 'denied' && phase !== 'init' && phase !== 'error' && (
397
- <ScanTopBar title={title} topInset={topInset} />
398
- )}
399
-
400
- {showChrome && (showTorch || !!pickImage || !!onClose) && (
401
- <ScanToolbar
402
- flash={torch}
403
- bottomInset={bottomInset}
404
- onFlash={showTorch ? () => setTorch((t) => !t) : undefined}
405
- onAlbum={pickImage ? onAlbum : undefined}
406
- onClose={onClose}
407
- />
408
- )}
409
-
410
- {phase === 'success' && product && (
411
- <ResultFocus
412
- product={product}
413
- detectMs={detectMs}
414
- bottomInset={bottomInset}
415
- onRescan={reset}
416
- onConfirm={onConfirmPress}
417
- />
418
- )}
419
-
420
- {phase === 'fail' && (
421
- <ResultFail bottomInset={bottomInset} onRescan={reset} />
422
- )}
423
-
424
- {phase === 'error' && (
425
- <ScanErrorOverlay onClose={onClose} onRetry={retryCamera} />
426
- )}
427
-
428
- {phase === 'denied' && (
429
- <DeniedOverlay onClose={onClose} onSettings={openSettings} />
430
- )}
431
- </View>
11
+ const insets = useContext(SafeAreaInsetsContext);
12
+ return insets ? (
13
+ <ScannerInner {...props} />
14
+ ) : (
15
+ <SafeAreaProvider>
16
+ <ScannerInner {...props} />
17
+ </SafeAreaProvider>
432
18
  );
433
19
  }
434
-
435
- const styles = StyleSheet.create({
436
- root: { flex: 1, backgroundColor: scanChrome.cameraBg },
437
- });
@@ -0,0 +1,26 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { Pressable, Text, View } from 'react-native';
3
+ import { unsupportedScanError } from '../errors';
4
+ import type { ScannerProps } from './types';
5
+
6
+ export function Scanner({ onError, onClose }: ScannerProps) {
7
+ const callback = useRef(onError);
8
+ callback.current = onError;
9
+ useEffect(() => {
10
+ callback.current?.(unsupportedScanError());
11
+ }, []);
12
+ return (
13
+ <View>
14
+ <Text>HMS Scan 仅支持 Android 与 iOS 设备</Text>
15
+ {onClose && (
16
+ <Pressable
17
+ accessibilityRole="button"
18
+ accessibilityLabel="返回"
19
+ onPress={onClose}
20
+ >
21
+ <Text>返回</Text>
22
+ </Pressable>
23
+ )}
24
+ </View>
25
+ );
26
+ }