@unif/react-native-hms-scan 0.6.1 → 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.
- package/README.md +24 -72
- package/android/src/main/AndroidManifest.xml +0 -9
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanImage.kt +135 -0
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanModule.kt +163 -147
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanResultMapper.kt +17 -9
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanView.kt +57 -19
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanViewManager.kt +1 -1
- package/ios/HmsScanModule.mm +13 -37
- package/ios/HmsScanResultMapper.h +4 -3
- package/ios/HmsScanResultMapper.mm +31 -19
- package/ios/HmsScanView.h +1 -1
- package/ios/HmsScanView.mm +140 -25
- package/lib/module/HmsScanView/HmsScanView.js +66 -0
- package/lib/module/HmsScanView/HmsScanView.js.map +1 -0
- package/lib/module/HmsScanView/HmsScanView.web.js +26 -0
- package/lib/module/HmsScanView/HmsScanView.web.js.map +1 -0
- package/lib/module/HmsScanView/types.js +4 -0
- package/lib/module/HmsScanView/types.js.map +1 -0
- package/lib/module/HmsScanViewNativeComponent.ts +7 -5
- package/lib/module/NativeHmsScan.js +1 -1
- package/lib/module/NativeHmsScan.js.map +1 -1
- package/lib/module/ScanError.js +12 -0
- package/lib/module/ScanError.js.map +1 -0
- package/lib/module/Scanner/DeniedOverlay.js +1 -1
- package/lib/module/Scanner/DeniedOverlay.js.map +1 -1
- package/lib/module/Scanner/ResultFail.js +3 -2
- package/lib/module/Scanner/ResultFail.js.map +1 -1
- package/lib/module/Scanner/ResultFocus/ResultFocus.js +56 -0
- package/lib/module/Scanner/ResultFocus/ResultFocus.js.map +1 -0
- package/lib/module/Scanner/ResultFocus/styles.js +39 -0
- package/lib/module/Scanner/ResultFocus/styles.js.map +1 -0
- package/lib/module/Scanner/ResultFocus/types.js +4 -0
- package/lib/module/Scanner/ResultFocus/types.js.map +1 -0
- package/lib/module/Scanner/ScanHint.js +6 -3
- package/lib/module/Scanner/ScanHint.js.map +1 -1
- package/lib/module/Scanner/ScanToolbar.js +6 -3
- package/lib/module/Scanner/ScanToolbar.js.map +1 -1
- package/lib/module/Scanner/ScanTopBar.js +6 -3
- package/lib/module/Scanner/ScanTopBar.js.map +1 -1
- package/lib/module/Scanner/Scanner.js +9 -311
- package/lib/module/Scanner/Scanner.js.map +1 -1
- package/lib/module/Scanner/Scanner.web.js +29 -0
- package/lib/module/Scanner/Scanner.web.js.map +1 -0
- package/lib/module/Scanner/ScannerInner.js +310 -0
- package/lib/module/Scanner/ScannerInner.js.map +1 -0
- package/lib/module/Scanner/styles.js +11 -0
- package/lib/module/Scanner/styles.js.map +1 -0
- package/lib/module/Scanner/types.js +4 -0
- package/lib/module/Scanner/types.js.map +1 -0
- package/lib/module/constants.js +20 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/decodeImage.js +29 -24
- package/lib/module/decodeImage.js.map +1 -1
- package/lib/module/decodeImage.web.js +7 -0
- package/lib/module/decodeImage.web.js.map +1 -0
- package/lib/module/errors.js +24 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/format.js +53 -47
- package/lib/module/format.js.map +1 -1
- package/lib/module/index.js +5 -19
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +8 -0
- package/lib/module/index.web.js.map +1 -0
- package/lib/module/mock.js +2 -23
- package/lib/module/mock.js.map +1 -1
- package/lib/module/permissions.js +24 -20
- package/lib/module/permissions.js.map +1 -1
- package/lib/module/permissions.web.js +10 -0
- package/lib/module/permissions.web.js.map +1 -0
- package/lib/module/types.js +1 -45
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/src/HmsScanView/HmsScanView.d.ts +3 -0
- package/lib/typescript/src/HmsScanView/HmsScanView.d.ts.map +1 -0
- package/lib/typescript/src/HmsScanView/HmsScanView.web.d.ts +3 -0
- package/lib/typescript/src/HmsScanView/HmsScanView.web.d.ts.map +1 -0
- package/lib/typescript/src/HmsScanView/types.d.ts +12 -0
- package/lib/typescript/src/HmsScanView/types.d.ts.map +1 -0
- package/lib/typescript/src/HmsScanViewNativeComponent.d.ts +6 -4
- package/lib/typescript/src/HmsScanViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/NativeHmsScan.d.ts +3 -5
- package/lib/typescript/src/NativeHmsScan.d.ts.map +1 -1
- package/lib/typescript/src/ScanError.d.ts +7 -0
- package/lib/typescript/src/ScanError.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/DeniedOverlay.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/ResultFail.d.ts +2 -1
- package/lib/typescript/src/Scanner/ResultFail.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/ResultFocus/ResultFocus.d.ts +4 -0
- package/lib/typescript/src/Scanner/ResultFocus/ResultFocus.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ResultFocus/styles.d.ts +36 -0
- package/lib/typescript/src/Scanner/ResultFocus/styles.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ResultFocus/types.d.ts +8 -0
- package/lib/typescript/src/Scanner/ResultFocus/types.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ScanHint.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/ScanToolbar.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/ScanTopBar.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/Scanner.d.ts +2 -45
- package/lib/typescript/src/Scanner/Scanner.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/Scanner.web.d.ts +3 -0
- package/lib/typescript/src/Scanner/Scanner.web.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ScannerInner.d.ts +3 -0
- package/lib/typescript/src/Scanner/ScannerInner.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/styles.d.ts +7 -0
- package/lib/typescript/src/Scanner/styles.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/types.d.ts +20 -0
- package/lib/typescript/src/Scanner/types.d.ts.map +1 -0
- package/lib/typescript/src/constants.d.ts +8 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/decodeImage.d.ts +2 -12
- package/lib/typescript/src/decodeImage.d.ts.map +1 -1
- package/lib/typescript/src/decodeImage.web.d.ts +3 -0
- package/lib/typescript/src/decodeImage.web.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +6 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/format.d.ts +15 -12
- package/lib/typescript/src/format.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +6 -7
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.web.d.ts +9 -0
- package/lib/typescript/src/index.web.d.ts.map +1 -0
- package/lib/typescript/src/mock.d.ts +9 -8
- package/lib/typescript/src/mock.d.ts.map +1 -1
- package/lib/typescript/src/permissions.d.ts +4 -8
- package/lib/typescript/src/permissions.d.ts.map +1 -1
- package/lib/typescript/src/permissions.web.d.ts +4 -0
- package/lib/typescript/src/permissions.web.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +31 -56
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +9 -2
- package/src/HmsScanView/HmsScanView.tsx +67 -0
- package/src/HmsScanView/HmsScanView.web.tsx +22 -0
- package/src/HmsScanView/types.ts +17 -0
- package/src/HmsScanViewNativeComponent.ts +7 -5
- package/src/NativeHmsScan.ts +3 -5
- package/src/ScanError.ts +13 -0
- package/src/Scanner/DeniedOverlay.tsx +4 -2
- package/src/Scanner/ResultFail.tsx +23 -5
- package/src/Scanner/ResultFocus/ResultFocus.tsx +43 -0
- package/src/Scanner/ResultFocus/styles.ts +25 -0
- package/src/Scanner/ResultFocus/types.ts +8 -0
- package/src/Scanner/ScanHint.tsx +16 -11
- package/src/Scanner/ScanToolbar.tsx +35 -13
- package/src/Scanner/ScanTopBar.tsx +20 -12
- package/src/Scanner/Scanner.tsx +13 -436
- package/src/Scanner/Scanner.web.tsx +26 -0
- package/src/Scanner/ScannerInner.tsx +391 -0
- package/src/Scanner/styles.ts +6 -0
- package/src/Scanner/types.ts +36 -0
- package/src/constants.ts +55 -0
- package/src/decodeImage.ts +35 -29
- package/src/decodeImage.web.ts +8 -0
- package/src/errors.ts +26 -0
- package/src/format.ts +77 -76
- package/src/index.tsx +17 -32
- package/src/index.web.tsx +22 -0
- package/src/mock.ts +21 -29
- package/src/permissions.ts +33 -20
- package/src/permissions.web.ts +9 -0
- package/src/types.ts +38 -126
- package/lib/module/HmsScanView.js +0 -43
- package/lib/module/HmsScanView.js.map +0 -1
- package/lib/module/Scanner/ResultFocus.js +0 -171
- package/lib/module/Scanner/ResultFocus.js.map +0 -1
- package/lib/typescript/src/HmsScanView.d.ts +0 -33
- package/lib/typescript/src/HmsScanView.d.ts.map +0 -1
- package/lib/typescript/src/Scanner/ResultFocus.d.ts +0 -14
- package/lib/typescript/src/Scanner/ResultFocus.d.ts.map +0 -1
- package/src/HmsScanView.tsx +0 -87
- 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,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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -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
|
+
};
|
package/src/decodeImage.ts
CHANGED
|
@@ -1,37 +1,43 @@
|
|
|
1
1
|
import NativeHmsScan from './NativeHmsScan';
|
|
2
2
|
import { formatsToCsv, parseResultsJson } from './format';
|
|
3
|
-
import {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
}
|