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:
2025-12-17 23:39:23 +05:30
parent 4461019a42
commit eb49b37b8a
6 changed files with 1652 additions and 0 deletions

View 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',
},
});

View 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',
},
});

View 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,
};
};

View 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,
];

View 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
View 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',
}