Implement Camera Scanner component and examples
- Added CameraScanner component for QR/Barcode scanning functionality. - Created CameraScannerExample to demonstrate usage of the CameraScanner. - Developed useCameraScanner hook for managing scanner state and events. - Introduced ScannerScreen for a drop-in ready scanner interface. - Added TypeScript types for ScanResult, ScannerError, and SDK configuration. - Implemented multiple examples showcasing various integration scenarios.
This commit is contained in:
275
src/components/CameraScanner.tsx
Normal file
275
src/components/CameraScanner.tsx
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
NativeModules,
|
||||||
|
NativeEventEmitter,
|
||||||
|
Alert,
|
||||||
|
ActivityIndicator,
|
||||||
|
} from 'react-native';
|
||||||
|
|
||||||
|
const { ICameraSDK } = NativeModules;
|
||||||
|
|
||||||
|
interface ScanResult {
|
||||||
|
code: string;
|
||||||
|
format: string;
|
||||||
|
formatCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScannerProps {
|
||||||
|
presignedUrl: string;
|
||||||
|
onScanResult?: (result: ScanResult) => void;
|
||||||
|
onError?: (error: string) => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CameraScanner: React.FC<ScannerProps> = ({
|
||||||
|
presignedUrl,
|
||||||
|
onScanResult,
|
||||||
|
onError,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const [isInitializing, setIsInitializing] = useState(true);
|
||||||
|
const [isScanning, setIsScanning] = useState(false);
|
||||||
|
const [lastScan, setLastScan] = useState<ScanResult | null>(null);
|
||||||
|
const eventEmitterRef = useRef<NativeEventEmitter | null>(null);
|
||||||
|
|
||||||
|
// Initialize SDK and setup event listeners
|
||||||
|
useEffect(() => {
|
||||||
|
initializeSDK();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stopScanning();
|
||||||
|
if (eventEmitterRef.current) {
|
||||||
|
eventEmitterRef.current.removeAllListeners();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [presignedUrl]);
|
||||||
|
|
||||||
|
const initializeSDK = async () => {
|
||||||
|
try {
|
||||||
|
if (!ICameraSDK) {
|
||||||
|
throw new Error('ICameraSDK module not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up event emitter
|
||||||
|
eventEmitterRef.current = new NativeEventEmitter(ICameraSDK);
|
||||||
|
|
||||||
|
// Check if SDK is already initialized
|
||||||
|
const isInitialized = await ICameraSDK.isInitialized();
|
||||||
|
|
||||||
|
if (!isInitialized) {
|
||||||
|
// Initialize with QR_CODE and BAR_CODE features
|
||||||
|
const initResult = await ICameraSDK.initialize(presignedUrl, [
|
||||||
|
'QR_CODE',
|
||||||
|
'BAR_CODE',
|
||||||
|
]);
|
||||||
|
console.log('SDK initialized:', initResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup event listeners
|
||||||
|
eventEmitterRef.current.addListener('onScannerResult', (result: ScanResult) => {
|
||||||
|
console.log('Scan result:', result);
|
||||||
|
setLastScan(result);
|
||||||
|
onScanResult?.(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventEmitterRef.current.addListener('onScannerError', (error: any) => {
|
||||||
|
console.error('Scanner error:', error);
|
||||||
|
onError?.(error.error || 'Unknown error');
|
||||||
|
Alert.alert('Scan Error', error.error || 'Failed to scan code');
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsInitializing(false);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error('Failed to initialize SDK:', errorMessage);
|
||||||
|
onError?.(errorMessage);
|
||||||
|
Alert.alert('Initialization Error', errorMessage);
|
||||||
|
setIsInitializing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startScanning = async () => {
|
||||||
|
try {
|
||||||
|
if (!ICameraSDK) {
|
||||||
|
throw new Error('ICameraSDK module not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ICameraSDK.startScanner();
|
||||||
|
console.log('Scanner started:', result);
|
||||||
|
setIsScanning(true);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error('Failed to start scanner:', errorMessage);
|
||||||
|
onError?.(errorMessage);
|
||||||
|
Alert.alert('Start Scanner Error', errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopScanning = async () => {
|
||||||
|
try {
|
||||||
|
if (!ICameraSDK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ICameraSDK.stopScanner();
|
||||||
|
console.log('Scanner stopped:', result);
|
||||||
|
setIsScanning(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to stop scanner:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = async () => {
|
||||||
|
await stopScanning();
|
||||||
|
onClose?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isInitializing) {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<ActivityIndicator size="large" color="#0000ff" />
|
||||||
|
<Text style={styles.loadingText}>Initializing Camera SDK...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.title}>QR/Barcode Scanner</Text>
|
||||||
|
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
|
||||||
|
<Text style={styles.closeButtonText}>✕</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Camera preview would go here */}
|
||||||
|
<View style={styles.cameraPreview}>
|
||||||
|
<Text style={styles.placeholderText}>
|
||||||
|
Camera Preview Area
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.controlsContainer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
isScanning ? styles.buttonActive : styles.buttonInactive,
|
||||||
|
]}
|
||||||
|
onPress={isScanning ? stopScanning : startScanning}
|
||||||
|
disabled={isInitializing}
|
||||||
|
>
|
||||||
|
<Text style={styles.buttonText}>
|
||||||
|
{isScanning ? '⏹ Stop Scanning' : '▶ Start Scanning'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{lastScan && (
|
||||||
|
<View style={styles.resultContainer}>
|
||||||
|
<Text style={styles.resultLabel}>Last Scan Result:</Text>
|
||||||
|
<Text style={styles.resultCode}>{lastScan.code}</Text>
|
||||||
|
<Text style={styles.resultFormat}>Format: {lastScan.format}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#e0e0e0',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#000',
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
closeButtonText: {
|
||||||
|
fontSize: 24,
|
||||||
|
color: '#999',
|
||||||
|
},
|
||||||
|
cameraPreview: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#000',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
margin: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#ddd',
|
||||||
|
},
|
||||||
|
placeholderText: {
|
||||||
|
color: '#999',
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
controlsContainer: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
borderRadius: 8,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
buttonActive: {
|
||||||
|
backgroundColor: '#ff4444',
|
||||||
|
},
|
||||||
|
buttonInactive: {
|
||||||
|
backgroundColor: '#4CAF50',
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
resultContainer: {
|
||||||
|
marginHorizontal: 16,
|
||||||
|
marginBottom: 16,
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: '#e8f5e9',
|
||||||
|
borderRadius: 8,
|
||||||
|
borderLeftWidth: 4,
|
||||||
|
borderLeftColor: '#4CAF50',
|
||||||
|
},
|
||||||
|
resultLabel: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#666',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
resultCode: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#000',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
resultFormat: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
marginTop: 12,
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
});
|
||||||
340
src/components/CameraScannerExample.tsx
Normal file
340
src/components/CameraScannerExample.tsx
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
Alert,
|
||||||
|
ScrollView,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useCameraScanner, ScanResult } from '../hooks/useCameraScanner';
|
||||||
|
|
||||||
|
interface ScanHistoryItem {
|
||||||
|
code: string;
|
||||||
|
format: string;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CameraScannerExample: React.FC = () => {
|
||||||
|
const [showScanner, setShowScanner] = useState(false);
|
||||||
|
const [scanHistory, setScanHistory] = useState<ScanHistoryItem[]>([]);
|
||||||
|
|
||||||
|
// Your presigned URL - replace with actual URL from your backend
|
||||||
|
const PRESIGNED_URL = 'https://your-presigned-url-here.com';
|
||||||
|
|
||||||
|
const scanner = useCameraScanner({
|
||||||
|
presignedUrl: PRESIGNED_URL,
|
||||||
|
features: ['QR_CODE', 'BAR_CODE'],
|
||||||
|
onScanResult: (result: ScanResult) => {
|
||||||
|
// Add to history
|
||||||
|
const item: ScanHistoryItem = {
|
||||||
|
code: result.code,
|
||||||
|
format: result.format,
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
};
|
||||||
|
setScanHistory((prev) => [item, ...prev]);
|
||||||
|
|
||||||
|
// Show alert
|
||||||
|
Alert.alert('Scan Successful!', `Code: ${result.code}\nFormat: ${result.format}`);
|
||||||
|
},
|
||||||
|
onError: (error: string) => {
|
||||||
|
Alert.alert('Error', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleStartScanning = async () => {
|
||||||
|
if (!scanner.isInitialized) {
|
||||||
|
Alert.alert('Error', 'Scanner is not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setShowScanner(true);
|
||||||
|
await scanner.startScanning();
|
||||||
|
} catch (err) {
|
||||||
|
Alert.alert('Error', 'Failed to start scanner');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStopScanning = async () => {
|
||||||
|
await scanner.stopScanning();
|
||||||
|
setShowScanner(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearHistory = () => {
|
||||||
|
setScanHistory([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.headerTitle}>Camera Scanner</Text>
|
||||||
|
<Text style={styles.headerSubtitle}>QR Code & Barcode Scanner</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{showScanner ? (
|
||||||
|
<View style={styles.scannerContainer}>
|
||||||
|
<View style={styles.cameraPlaceholder}>
|
||||||
|
<Text style={styles.cameraPlaceholderText}>📷</Text>
|
||||||
|
<Text style={styles.cameraStatusText}>
|
||||||
|
{scanner.isScanning ? 'Scanning...' : 'Ready to scan'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.stopButton}
|
||||||
|
onPress={handleStopScanning}
|
||||||
|
>
|
||||||
|
<Text style={styles.stopButtonText}>Close Scanner</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
|
||||||
|
{/* Status Section */}
|
||||||
|
<View style={styles.statusSection}>
|
||||||
|
<Text style={styles.sectionTitle}>Status</Text>
|
||||||
|
<View style={styles.statusCard}>
|
||||||
|
<View style={styles.statusItem}>
|
||||||
|
<Text style={styles.statusLabel}>SDK Initialized:</Text>
|
||||||
|
<Text style={styles.statusValue}>
|
||||||
|
{scanner.isInitialized ? '✓ Yes' : '✗ No'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.statusItem}>
|
||||||
|
<Text style={styles.statusLabel}>Scanning:</Text>
|
||||||
|
<Text style={styles.statusValue}>
|
||||||
|
{scanner.isScanning ? '✓ Active' : '✗ Inactive'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Controls Section */}
|
||||||
|
<View style={styles.controlsSection}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.startButton}
|
||||||
|
onPress={handleStartScanning}
|
||||||
|
disabled={!scanner.isInitialized}
|
||||||
|
>
|
||||||
|
<Text style={styles.startButtonText}>▶ Start Scanner</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* History Section */}
|
||||||
|
<View style={styles.historySection}>
|
||||||
|
<View style={styles.historyHeader}>
|
||||||
|
<Text style={styles.sectionTitle}>Scan History</Text>
|
||||||
|
{scanHistory.length > 0 && (
|
||||||
|
<TouchableOpacity onPress={clearHistory}>
|
||||||
|
<Text style={styles.clearButton}>Clear</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{scanHistory.length === 0 ? (
|
||||||
|
<View style={styles.emptyState}>
|
||||||
|
<Text style={styles.emptyStateText}>No scans yet</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View>
|
||||||
|
{scanHistory.map((item, index) => (
|
||||||
|
<View key={index} style={styles.historyItem}>
|
||||||
|
<View style={styles.historyItemContent}>
|
||||||
|
<Text style={styles.historyCode}>{item.code}</Text>
|
||||||
|
<View style={styles.historyFooter}>
|
||||||
|
<Text style={styles.historyFormat}>
|
||||||
|
{item.format}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.historyTime}>{item.timestamp}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{scanner.error && (
|
||||||
|
<View style={styles.errorBanner}>
|
||||||
|
<Text style={styles.errorText}>⚠ {scanner.error}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
backgroundColor: '#2c3e50',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingTop: 20,
|
||||||
|
paddingBottom: 16,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
headerSubtitle: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#bdc3c7',
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
scannerContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
cameraPlaceholder: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
cameraPlaceholderText: {
|
||||||
|
fontSize: 80,
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
cameraStatusText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#fff',
|
||||||
|
marginTop: 12,
|
||||||
|
},
|
||||||
|
stopButton: {
|
||||||
|
marginHorizontal: 16,
|
||||||
|
marginBottom: 20,
|
||||||
|
paddingVertical: 14,
|
||||||
|
backgroundColor: '#e74c3c',
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
stopButtonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
statusSection: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#2c3e50',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
statusCard: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
borderLeftWidth: 4,
|
||||||
|
borderLeftColor: '#3498db',
|
||||||
|
},
|
||||||
|
statusItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#ecf0f1',
|
||||||
|
},
|
||||||
|
statusLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#555',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
statusValue: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#27ae60',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
controlsSection: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
startButton: {
|
||||||
|
paddingVertical: 14,
|
||||||
|
backgroundColor: '#27ae60',
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
startButtonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
historySection: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
historyHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
clearButton: {
|
||||||
|
color: '#e74c3c',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
emptyState: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 32,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
emptyStateText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#999',
|
||||||
|
},
|
||||||
|
historyItem: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
marginBottom: 8,
|
||||||
|
borderLeftWidth: 4,
|
||||||
|
borderLeftColor: '#27ae60',
|
||||||
|
},
|
||||||
|
historyItemContent: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
historyCode: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#2c3e50',
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
|
historyFooter: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
historyFormat: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#666',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
historyTime: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#999',
|
||||||
|
},
|
||||||
|
errorBanner: {
|
||||||
|
backgroundColor: '#ffe74c',
|
||||||
|
padding: 12,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#ffd700',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#856404',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
});
|
||||||
145
src/hooks/useCameraScanner.ts
Normal file
145
src/hooks/useCameraScanner.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { useEffect, useState, useRef } from 'react';
|
||||||
|
import { NativeModules, NativeEventEmitter } from 'react-native';
|
||||||
|
|
||||||
|
const { ICameraSDK } = NativeModules;
|
||||||
|
|
||||||
|
export interface ScanResult {
|
||||||
|
code: string;
|
||||||
|
format: string;
|
||||||
|
formatCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseCameraScannerOptions {
|
||||||
|
presignedUrl: string;
|
||||||
|
features?: string[];
|
||||||
|
onScanResult?: (result: ScanResult) => void;
|
||||||
|
onError?: (error: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCameraScanner = (options: UseCameraScannerOptions) => {
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
const [isScanning, setIsScanning] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const eventEmitterRef = useRef<NativeEventEmitter | null>(null);
|
||||||
|
const subscriptionsRef = useRef<any[]>([]);
|
||||||
|
|
||||||
|
// Initialize the SDK
|
||||||
|
useEffect(() => {
|
||||||
|
initialize();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
}, [options.presignedUrl]);
|
||||||
|
|
||||||
|
const initialize = async () => {
|
||||||
|
try {
|
||||||
|
if (!ICameraSDK) {
|
||||||
|
throw new Error('ICameraSDK module not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
eventEmitterRef.current = new NativeEventEmitter(ICameraSDK);
|
||||||
|
|
||||||
|
const isInit = await ICameraSDK.isInitialized();
|
||||||
|
|
||||||
|
if (!isInit) {
|
||||||
|
const features = options.features || ['QR_CODE', 'BAR_CODE'];
|
||||||
|
await ICameraSDK.initialize(options.presignedUrl, features);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup event listeners
|
||||||
|
const scannerResultSub = eventEmitterRef.current.addListener(
|
||||||
|
'onScannerResult',
|
||||||
|
(result: ScanResult) => {
|
||||||
|
options.onScanResult?.(result);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const scannerErrorSub = eventEmitterRef.current.addListener(
|
||||||
|
'onScannerError',
|
||||||
|
(error: any) => {
|
||||||
|
const errorMsg = error.error || 'Unknown error';
|
||||||
|
setError(errorMsg);
|
||||||
|
options.onError?.(errorMsg);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
subscriptionsRef.current = [scannerResultSub, scannerErrorSub];
|
||||||
|
setIsInitialized(true);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
setError(errorMsg);
|
||||||
|
options.onError?.(errorMsg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startScanning = async () => {
|
||||||
|
try {
|
||||||
|
if (!ICameraSDK) {
|
||||||
|
throw new Error('ICameraSDK module not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
await ICameraSDK.startScanner();
|
||||||
|
setIsScanning(true);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
setError(errorMsg);
|
||||||
|
options.onError?.(errorMsg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopScanning = async () => {
|
||||||
|
try {
|
||||||
|
if (!ICameraSDK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ICameraSDK.stopScanner();
|
||||||
|
setIsScanning(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error stopping scanner:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
await stopScanning();
|
||||||
|
|
||||||
|
subscriptionsRef.current.forEach((sub) => {
|
||||||
|
sub.remove();
|
||||||
|
});
|
||||||
|
subscriptionsRef.current = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (ICameraSDK) {
|
||||||
|
await ICameraSDK.shutdown();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error shutting down SDK:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConfiguration = async () => {
|
||||||
|
try {
|
||||||
|
if (!ICameraSDK) {
|
||||||
|
throw new Error('ICameraSDK module not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await ICameraSDK.getConfiguration();
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
setError(errorMsg);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isInitialized,
|
||||||
|
isScanning,
|
||||||
|
error,
|
||||||
|
startScanning,
|
||||||
|
stopScanning,
|
||||||
|
getConfiguration,
|
||||||
|
cleanup,
|
||||||
|
};
|
||||||
|
};
|
||||||
411
src/screens/CameraExamples.tsx
Normal file
411
src/screens/CameraExamples.tsx
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
/**
|
||||||
|
* Complete Examples for iCameraSDK
|
||||||
|
* Copy and adapt these examples for your use case
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
Alert,
|
||||||
|
StyleSheet,
|
||||||
|
NativeModules,
|
||||||
|
NativeEventEmitter,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useCameraScanner } from '../hooks/useCameraScanner';
|
||||||
|
import { ScanResult } from '../types/icamera';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Example 1: Using the Hook (Recommended - Easiest)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const Example1_UseHook = () => {
|
||||||
|
const scanner = useCameraScanner({
|
||||||
|
presignedUrl: 'https://your-backend-presigned-url.com',
|
||||||
|
features: ['QR_CODE', 'BAR_CODE'],
|
||||||
|
onScanResult: (result: ScanResult) => {
|
||||||
|
console.log('✓ Scanned:', result.code);
|
||||||
|
Alert.alert('Success', `Code: ${result.code}`);
|
||||||
|
},
|
||||||
|
onError: (error: string) => {
|
||||||
|
console.error('✗ Error:', error);
|
||||||
|
Alert.alert('Error', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text>Initialized: {scanner.isInitialized ? '✓' : '✗'}</Text>
|
||||||
|
<TouchableOpacity onPress={scanner.startScanning}>
|
||||||
|
<Text style={styles.button}>Start Scan</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Example 2: Direct NativeModule Access
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const Example2_DirectModule = () => {
|
||||||
|
const [isScanning, setIsScanning] = useState(false);
|
||||||
|
const { ICameraSDK } = NativeModules;
|
||||||
|
|
||||||
|
const handleStart = async () => {
|
||||||
|
try {
|
||||||
|
// Initialize if not already done
|
||||||
|
const isInit = await ICameraSDK.isInitialized();
|
||||||
|
if (!isInit) {
|
||||||
|
await ICameraSDK.initialize(
|
||||||
|
'https://presigned-url.com',
|
||||||
|
['QR_CODE', 'BAR_CODE']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start scanner
|
||||||
|
await ICameraSDK.startScanner();
|
||||||
|
setIsScanning(true);
|
||||||
|
|
||||||
|
// Setup listeners
|
||||||
|
const emitter = new NativeEventEmitter(ICameraSDK);
|
||||||
|
emitter.addListener('onScannerResult', (result) => {
|
||||||
|
console.log('Scanned:', result.code);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStop = async () => {
|
||||||
|
try {
|
||||||
|
await ICameraSDK.stopScanner();
|
||||||
|
setIsScanning(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<TouchableOpacity onPress={isScanning ? handleStop : handleStart}>
|
||||||
|
<Text style={styles.button}>
|
||||||
|
{isScanning ? 'Stop' : 'Start'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Example 3: Advanced Hook Usage with Lifecycle
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const Example3_AdvancedHook = () => {
|
||||||
|
const [scanHistory, setScanHistory] = useState<ScanResult[]>([]);
|
||||||
|
|
||||||
|
const scanner = useCameraScanner({
|
||||||
|
presignedUrl: 'https://your-backend-presigned-url.com',
|
||||||
|
features: ['QR_CODE'],
|
||||||
|
onScanResult: (result: ScanResult) => {
|
||||||
|
setScanHistory((prev) => [result, ...prev]);
|
||||||
|
},
|
||||||
|
onError: (error: string) => {
|
||||||
|
Alert.alert('Scanner Error', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
scanner.cleanup();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.status}>
|
||||||
|
<Text>SDK: {scanner.isInitialized ? '✓ Initialized' : '⏳ Loading'}</Text>
|
||||||
|
<Text>State: {scanner.isScanning ? '▶ Scanning' : '⏸ Idle'}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={scanner.startScanning}
|
||||||
|
disabled={scanner.isScanning}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
<Text>▶ Start Scanning</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={scanner.stopScanning}
|
||||||
|
disabled={!scanner.isScanning}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
<Text>⏹ Stop Scanning</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.history}>
|
||||||
|
<Text style={styles.historyTitle}>
|
||||||
|
Scan History ({scanHistory.length})
|
||||||
|
</Text>
|
||||||
|
{scanHistory.map((scan, idx) => (
|
||||||
|
<View key={idx} style={styles.historyItem}>
|
||||||
|
<Text>{scan.code}</Text>
|
||||||
|
<Text style={styles.historyFormat}>{scan.format}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{scanner.error && (
|
||||||
|
<View style={styles.error}>
|
||||||
|
<Text style={styles.errorText}>{scanner.error}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Example 4: Integration with Your App Flow
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Example4_AppIntegration = () => {
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [scannedData, setScannedData] = useState<ScanResult | null>(null);
|
||||||
|
|
||||||
|
// Your backend API to get presigned URL
|
||||||
|
const getPresignedUrl = async (userId: string): Promise<string> => {
|
||||||
|
const response = await fetch(`/api/camera/presigned-url?userId=${userId}`);
|
||||||
|
const data = await response.json();
|
||||||
|
return data.presignedUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scanner = useCameraScanner({
|
||||||
|
presignedUrl: user ? getPresignedUrl(user.id) : '',
|
||||||
|
onScanResult: async (result: ScanResult) => {
|
||||||
|
setScannedData(result);
|
||||||
|
|
||||||
|
// Process the scanned data
|
||||||
|
try {
|
||||||
|
// Send to your backend
|
||||||
|
const response = await fetch('/api/camera/process-scan', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
userId: user?.id,
|
||||||
|
scannedCode: result.code,
|
||||||
|
format: result.format,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Alert.alert('✓ Success', 'Scan processed successfully');
|
||||||
|
await scanner.stopScanning();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('✗ Error', 'Failed to process scan');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error: string) => {
|
||||||
|
Alert.alert('Scanner Error', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleStartScanning = async () => {
|
||||||
|
if (!user) {
|
||||||
|
Alert.alert('Error', 'User not logged in');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scanner.isInitialized) {
|
||||||
|
Alert.alert('Error', 'Scanner not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await scanner.startScanning();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.title}>App Integration Example</Text>
|
||||||
|
|
||||||
|
{user ? (
|
||||||
|
<View>
|
||||||
|
<Text>Logged in as: {user.email}</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={handleStartScanning}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
<Text>Open Scanner</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
{scannedData && (
|
||||||
|
<View style={styles.result}>
|
||||||
|
<Text>Last scan: {scannedData.code}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => setUser({ id: '1', email: 'user@example.com' })}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
<Text>Login First</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Example 5: Error Handling & Recovery
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const Example5_ErrorHandling = () => {
|
||||||
|
const [logs, setLogs] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const addLog = (message: string) => {
|
||||||
|
setLogs((prev) => [`${new Date().toLocaleTimeString()} - ${message}`, ...prev]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scanner = useCameraScanner({
|
||||||
|
presignedUrl: 'https://your-presigned-url.com',
|
||||||
|
onScanResult: (result: ScanResult) => {
|
||||||
|
addLog(`✓ Scanned: ${result.code}`);
|
||||||
|
},
|
||||||
|
onError: (error: string) => {
|
||||||
|
addLog(`✗ Error: ${error}`);
|
||||||
|
|
||||||
|
// Handle specific errors
|
||||||
|
if (error.includes('permission')) {
|
||||||
|
addLog('💡 Tip: Grant camera permissions in settings');
|
||||||
|
} else if (error.includes('not initialized')) {
|
||||||
|
addLog('💡 Tip: Initialize SDK first');
|
||||||
|
} else if (error.includes('camera')) {
|
||||||
|
addLog('💡 Tip: Ensure camera is available');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.title}>Error Handling Example</Text>
|
||||||
|
|
||||||
|
<View style={styles.logs}>
|
||||||
|
<Text style={styles.logsTitle}>Logs:</Text>
|
||||||
|
{logs.map((log, idx) => (
|
||||||
|
<Text key={idx} style={styles.log}>
|
||||||
|
{log}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={scanner.startScanning}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
<Text>Test Scanner</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Styles
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 16,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
backgroundColor: '#2196F3',
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginVertical: 8,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
marginTop: 16,
|
||||||
|
},
|
||||||
|
historyTitle: {
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
historyItem: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
padding: 8,
|
||||||
|
marginBottom: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
historyFormat: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
backgroundColor: '#e8f5e9',
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginTop: 12,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
backgroundColor: '#ffe0e0',
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginTop: 12,
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: '#c00',
|
||||||
|
},
|
||||||
|
logs: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
maxHeight: 200,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
logsTitle: {
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
log: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: '#666',
|
||||||
|
marginBottom: 4,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Export all examples
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export default [
|
||||||
|
Example1_UseHook,
|
||||||
|
Example2_DirectModule,
|
||||||
|
Example3_AdvancedHook,
|
||||||
|
Example4_AppIntegration,
|
||||||
|
Example5_ErrorHandling,
|
||||||
|
];
|
||||||
227
src/screens/ScannerScreen.tsx
Normal file
227
src/screens/ScannerScreen.tsx
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
StyleSheet,
|
||||||
|
Alert,
|
||||||
|
ScrollView,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useCameraScanner, ScanResult } from '../hooks/useCameraScanner';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop-in ready Scanner Screen
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* <ScannerScreen presignedUrl="YOUR_URL" onScan={handleScan} />
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ScannerScreenProps {
|
||||||
|
presignedUrl: string;
|
||||||
|
onScan?: (result: ScanResult) => void;
|
||||||
|
onError?: (error: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScannerScreen: React.FC<ScannerScreenProps> = ({
|
||||||
|
presignedUrl,
|
||||||
|
onScan,
|
||||||
|
onError,
|
||||||
|
}) => {
|
||||||
|
const [scans, setScans] = useState<ScanResult[]>([]);
|
||||||
|
|
||||||
|
const scanner = useCameraScanner({
|
||||||
|
presignedUrl,
|
||||||
|
features: ['QR_CODE', 'BAR_CODE'],
|
||||||
|
onScanResult: (result) => {
|
||||||
|
console.log('✓ Scan:', result);
|
||||||
|
setScans((prev) => [result, ...prev]);
|
||||||
|
onScan?.(result);
|
||||||
|
|
||||||
|
// Optional: Show alert
|
||||||
|
Alert.alert(
|
||||||
|
'✓ Scanned Successfully',
|
||||||
|
`Code: ${result.code}\nFormat: ${result.format}`,
|
||||||
|
[{ text: 'OK' }]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('✗ Error:', error);
|
||||||
|
onError?.(error);
|
||||||
|
Alert.alert('Error', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.title}>Scanner</Text>
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
|
{scanner.isInitialized ? '✓ Ready' : '⏳ Loading...'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Camera Area */}
|
||||||
|
<View style={styles.cameraContainer}>
|
||||||
|
<View style={styles.cameraPlaceholder}>
|
||||||
|
<Text style={styles.cameraIcon}>📷</Text>
|
||||||
|
<Text style={styles.cameraStatus}>
|
||||||
|
{scanner.isScanning ? '🔴 Scanning...' : '⚪ Ready'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
<View style={styles.controls}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
scanner.isScanning ? styles.stopButton : styles.startButton,
|
||||||
|
]}
|
||||||
|
onPress={
|
||||||
|
scanner.isScanning ? scanner.stopScanning : scanner.startScanning
|
||||||
|
}
|
||||||
|
disabled={!scanner.isInitialized}
|
||||||
|
>
|
||||||
|
<Text style={styles.buttonText}>
|
||||||
|
{scanner.isScanning ? '⏹ Stop' : '▶ Start'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Results */}
|
||||||
|
{scans.length > 0 && (
|
||||||
|
<View style={styles.results}>
|
||||||
|
<Text style={styles.resultsTitle}>Results ({scans.length})</Text>
|
||||||
|
<ScrollView style={styles.resultsList}>
|
||||||
|
{scans.map((scan, idx) => (
|
||||||
|
<View key={idx} style={styles.resultItem}>
|
||||||
|
<Text style={styles.resultCode}>{scan.code}</Text>
|
||||||
|
<Text style={styles.resultFormat}>{scan.format}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error Display */}
|
||||||
|
{scanner.error && (
|
||||||
|
<View style={styles.error}>
|
||||||
|
<Text style={styles.errorText}>⚠ {scanner.error}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
backgroundColor: '#2c3e50',
|
||||||
|
padding: 16,
|
||||||
|
paddingTop: 20,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#bdc3c7',
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
cameraContainer: {
|
||||||
|
flex: 2,
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
cameraPlaceholder: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#1a1a1a',
|
||||||
|
borderRadius: 12,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#444',
|
||||||
|
},
|
||||||
|
cameraIcon: {
|
||||||
|
fontSize: 60,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
cameraStatus: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#999',
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
padding: 16,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#eee',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
startButton: {
|
||||||
|
backgroundColor: '#27ae60',
|
||||||
|
},
|
||||||
|
stopButton: {
|
||||||
|
backgroundColor: '#e74c3c',
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
results: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 16,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#eee',
|
||||||
|
},
|
||||||
|
resultsTitle: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#333',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
resultsList: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
resultItem: {
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
marginBottom: 6,
|
||||||
|
backgroundColor: '#f0f0f0',
|
||||||
|
borderLeftWidth: 3,
|
||||||
|
borderLeftColor: '#27ae60',
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
resultCode: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#000',
|
||||||
|
},
|
||||||
|
resultFormat: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#666',
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
backgroundColor: '#ffe0e0',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#ffcccc',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#c00',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
});
|
||||||
254
src/types/icamera.ts
Normal file
254
src/types/icamera.ts
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
/**
|
||||||
|
* iCameraSDK TypeScript Types
|
||||||
|
* Type definitions for the iCameraSDK React Native integration
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ScanResult {
|
||||||
|
/**
|
||||||
|
* The scanned code/data
|
||||||
|
*/
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format name of the scanned code
|
||||||
|
* Examples: "QR_CODE", "CODE_128", "CODE_39", "EAN_13", "EAN_8"
|
||||||
|
*/
|
||||||
|
format: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Numeric format code
|
||||||
|
* QR_CODE: 32, CODE_128: 64, etc.
|
||||||
|
*/
|
||||||
|
formatCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScannerError {
|
||||||
|
/**
|
||||||
|
* Error message
|
||||||
|
*/
|
||||||
|
error: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The error class name
|
||||||
|
*/
|
||||||
|
errorType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SDKConfig {
|
||||||
|
/**
|
||||||
|
* The presigned URL for uploads
|
||||||
|
*/
|
||||||
|
presignedUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of enabled features
|
||||||
|
*/
|
||||||
|
features: CaptureFeature[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CaptureFeature = 'QR_CODE' | 'BAR_CODE' | 'CAPTURE_3D' | 'LENS';
|
||||||
|
|
||||||
|
export interface UseCameraScannerOptions {
|
||||||
|
/**
|
||||||
|
* Presigned URL for the SDK
|
||||||
|
* Should be generated from your backend
|
||||||
|
*/
|
||||||
|
presignedUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Features to enable
|
||||||
|
* @default ['QR_CODE', 'BAR_CODE']
|
||||||
|
*/
|
||||||
|
features?: CaptureFeature[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when a code is successfully scanned
|
||||||
|
*/
|
||||||
|
onScanResult?: (result: ScanResult) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when an error occurs
|
||||||
|
*/
|
||||||
|
onError?: (error: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseCameraScannerReturn {
|
||||||
|
/**
|
||||||
|
* Whether the SDK is initialized
|
||||||
|
*/
|
||||||
|
isInitialized: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether scanning is currently active
|
||||||
|
*/
|
||||||
|
isScanning: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current error message, if any
|
||||||
|
*/
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the scanner
|
||||||
|
*/
|
||||||
|
startScanning: () => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the scanner
|
||||||
|
*/
|
||||||
|
stopScanning: () => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current SDK configuration
|
||||||
|
*/
|
||||||
|
getConfiguration: () => Promise<SDKConfig>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up and shutdown the SDK
|
||||||
|
*/
|
||||||
|
cleanup: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CameraScannerProps {
|
||||||
|
/**
|
||||||
|
* Presigned URL for the SDK
|
||||||
|
*/
|
||||||
|
presignedUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when a code is scanned
|
||||||
|
*/
|
||||||
|
onScanResult?: (result: ScanResult) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when an error occurs
|
||||||
|
*/
|
||||||
|
onError?: (error: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when user closes the scanner
|
||||||
|
*/
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScannerScreenProps {
|
||||||
|
/**
|
||||||
|
* Presigned URL for the SDK
|
||||||
|
*/
|
||||||
|
presignedUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when a code is scanned
|
||||||
|
*/
|
||||||
|
onScan?: (result: ScanResult) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when an error occurs
|
||||||
|
*/
|
||||||
|
onError?: (error: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Native module interface
|
||||||
|
* Available as: const { ICameraSDK } = NativeModules;
|
||||||
|
*/
|
||||||
|
export interface ICameraSDKModule {
|
||||||
|
/**
|
||||||
|
* Initialize the SDK with presigned URL and features
|
||||||
|
*/
|
||||||
|
initialize(presignedUrl: string, features: string[]): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the scanner
|
||||||
|
*/
|
||||||
|
startScanner(): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the scanner
|
||||||
|
*/
|
||||||
|
stopScanner(): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if SDK is initialized
|
||||||
|
*/
|
||||||
|
isInitialized(): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current SDK configuration
|
||||||
|
*/
|
||||||
|
getConfiguration(): Promise<SDKConfig>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shutdown the SDK
|
||||||
|
*/
|
||||||
|
shutdown(): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add event listener (Native Module EventEmitter)
|
||||||
|
*/
|
||||||
|
addListener(eventType: string, listener: (...args: any[]) => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove event listener
|
||||||
|
*/
|
||||||
|
removeListener(eventType: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all listeners
|
||||||
|
*/
|
||||||
|
removeAllListeners(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Barcode format constants
|
||||||
|
*/
|
||||||
|
export const BarcodeFormats = {
|
||||||
|
QR_CODE: 'QR_CODE',
|
||||||
|
CODE_128: 'CODE_128',
|
||||||
|
CODE_39: 'CODE_39',
|
||||||
|
CODE_93: 'CODE_93',
|
||||||
|
CODABAR: 'CODABAR',
|
||||||
|
DATA_MATRIX: 'DATA_MATRIX',
|
||||||
|
EAN_13: 'EAN_13',
|
||||||
|
EAN_8: 'EAN_8',
|
||||||
|
ITF: 'ITF',
|
||||||
|
PDF_417: 'PDF_417',
|
||||||
|
UPC_A: 'UPC_A',
|
||||||
|
UPC_E: 'UPC_E',
|
||||||
|
UNKNOWN: 'UNKNOWN',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type BarcodeFormat = typeof BarcodeFormats[keyof typeof BarcodeFormats];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture features available in the SDK
|
||||||
|
*/
|
||||||
|
export const CaptureFeatures = {
|
||||||
|
QR_CODE: 'QR_CODE' as CaptureFeature,
|
||||||
|
BAR_CODE: 'BAR_CODE' as CaptureFeature,
|
||||||
|
CAPTURE_3D: 'CAPTURE_3D' as CaptureFeature,
|
||||||
|
LENS: 'LENS' as CaptureFeature,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event types emitted by the scanner
|
||||||
|
*/
|
||||||
|
export const ScannerEvents = {
|
||||||
|
RESULT: 'onScannerResult',
|
||||||
|
ERROR: 'onScannerError',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ScannerEvent = typeof ScannerEvents[keyof typeof ScannerEvents];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error types
|
||||||
|
*/
|
||||||
|
export enum ScannerErrorType {
|
||||||
|
InitializationError = 'INIT_ERROR',
|
||||||
|
ScannerError = 'START_SCANNER_ERROR',
|
||||||
|
PermissionError = 'PERMISSION_ERROR',
|
||||||
|
LifecycleError = 'LIFECYCLE_ERROR',
|
||||||
|
ConfigError = 'CONFIG_ERROR',
|
||||||
|
NotInitializedError = 'NOT_INITIALIZED',
|
||||||
|
ShutdownError = 'SHUTDOWN_ERROR',
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user