Files
dt-nfc-identifier/src/screens/ScanScreen.tsx

397 lines
11 KiB
TypeScript

import React, { useCallback, useEffect } from 'react';
import { StyleSheet, View, Platform } from 'react-native';
import { Button, Text } from 'react-native-paper';
import { DTButton, DTColors } from 'react-native-dt-theme';
import type { ScanScreenProps } from '../types/navigation';
import { useScan } from '../hooks';
import { getScanInstructions } from '../services/nfc';
import { ScanAnimation } from '../components';
export function ScanScreen({ navigation }: ScanScreenProps) {
const {
state,
tag,
transponder,
error,
nfcStatus,
scanProgress,
startScan,
cancelScan,
openSettings,
} = useScan();
// Track if we've already navigated to prevent double navigation
const hasNavigated = React.useRef(false);
// Navigate to results when scan succeeds AND transponder is set
// We need transponder to be set (even if null for failed detection) before navigating
useEffect(() => {
// Only navigate once per scan session
if (hasNavigated.current) return;
// Wait for success state, tag data, AND transponder to be set
// transponder: undefined = not yet set, null = detection failed, Transponder = success
if (state === 'success' && tag && transponder !== undefined) {
hasNavigated.current = true;
console.log('[ScanScreen] Navigating to Result:', {
hasTag: !!tag,
hasTransponder: !!transponder,
transponderType: transponder?.type,
transponderChipName: transponder?.chipName,
});
navigation.replace('Result', {
tagData: {
uid: tag.uid,
techTypes: tag.techTypes,
sak: tag.sak,
atqa: tag.atqa,
ats: tag.ats,
historicalBytes: tag.historicalBytes,
},
transponder: transponder ?? undefined,
});
}
}, [state, tag, transponder, navigation]);
// Reset navigation flag when starting a new scan
useEffect(() => {
if (state === 'scanning') {
hasNavigated.current = false;
}
}, [state]);
// Auto-start scan when screen loads
useEffect(() => {
// Small delay to ensure screen is mounted
const timeout = setTimeout(() => {
startScan();
}, 300);
return () => clearTimeout(timeout);
// Only run once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleCancel = useCallback(async () => {
await cancelScan();
navigation.goBack();
}, [cancelScan, navigation]);
const handleRetry = useCallback(() => {
startScan();
}, [startScan]);
// Get contextual error hints based on error type
const getErrorHint = () => {
if (!error?.type) return null;
switch (error.type) {
case 'TAG_LOST':
return 'Hold the tag steady against the back of your phone until scanning completes.';
case 'TIMEOUT':
return 'The tag took too long to respond. Try repositioning it closer to the NFC reader.';
case 'SCAN_CANCELLED':
return null; // User cancelled, no hint needed
case 'NFC_NOT_ENABLED':
return Platform.OS === 'ios'
? 'Go to Settings > NFC to enable NFC scanning.'
: 'Enable NFC in your device settings.';
case 'PERMISSION_DENIED':
return 'NFC permission was denied. Please grant NFC access in your device settings.';
case 'UNKNOWN':
return 'An unexpected error occurred. Make sure the tag is positioned correctly and try again.';
default:
return 'Make sure the tag is positioned correctly and try again.';
}
};
// Render NFC not supported state
if (!nfcStatus.isSupported && state !== 'scanning') {
return (
<View style={styles.container}>
<View style={styles.content}>
<View style={styles.errorIcon}>
<Text style={styles.errorIconText}></Text>
</View>
<Text variant="headlineMedium" style={styles.errorText}>
NFC NOT SUPPORTED
</Text>
<Text variant="bodyLarge" style={styles.errorMessage}>
This device does not have NFC hardware.
</Text>
<Text variant="bodyMedium" style={styles.hintText}>
NFC scanning requires a device with NFC capability. Most modern smartphones have NFC, but it may need to be enabled in settings.
</Text>
</View>
<View style={styles.footer}>
<Button
mode="text"
onPress={() => navigation.goBack()}
labelStyle={styles.cancelLabel}>
GO BACK
</Button>
</View>
</View>
);
}
// Render NFC disabled state
if (!nfcStatus.isEnabled && error?.type === 'NFC_NOT_ENABLED') {
return (
<View style={styles.container}>
<View style={styles.content}>
<View style={styles.warningIcon}>
<Text style={styles.warningIconText}></Text>
</View>
<Text variant="headlineMedium" style={styles.warningText}>
NFC DISABLED
</Text>
<Text variant="bodyLarge" style={styles.errorMessage}>
NFC is turned off on this device.
</Text>
<Text variant="bodyMedium" style={styles.hintText}>
{Platform.OS === 'ios'
? 'Go to Settings and enable NFC scanning to continue.'
: 'Enable NFC in your device settings to scan tags.'}
</Text>
{Platform.OS === 'android' && (
<DTButton
variant="emphasis"
onPress={openSettings}
style={styles.settingsButton}>
OPEN SETTINGS
</DTButton>
)}
</View>
<View style={styles.footer}>
<Button
mode="text"
onPress={handleRetry}
labelStyle={styles.retryLabel}>
TRY AGAIN
</Button>
<Button
mode="text"
onPress={() => navigation.goBack()}
labelStyle={styles.cancelLabel}>
CANCEL
</Button>
</View>
</View>
);
}
return (
<View style={styles.container}>
<View style={styles.content}>
{state === 'scanning' && (
<>
<ScanAnimation isActive={true} size={180} />
<Text variant="headlineMedium" style={styles.scanningText}>
SCANNING
</Text>
<Text variant="bodyLarge" style={styles.instructionText}>
{getScanInstructions()}
</Text>
<Text variant="headlineSmall" style={{ paddingTop: 15, paddingBottom: 10 }}>
Not Scanning?
</Text>
<Text variant="bodyLarge" style={styles.instructionText}>
Phones can only read high frequency (13.56 MHz) -- low and ultra high frequency transponders cannot be scanned.
</Text>
{scanProgress && scanProgress.current > 1 && (
<Text variant="bodySmall" style={styles.progressText}>
{scanProgress.step}
</Text>
)}
</>
)}
{state === 'error' && (
<>
<View style={styles.errorIcon}>
<Text style={styles.errorIconText}>!</Text>
</View>
<Text variant="headlineMedium" style={styles.errorText}>
{error?.type === 'TAG_LOST'
? 'TAG LOST'
: error?.type === 'TIMEOUT'
? 'TIMEOUT'
: error?.type === 'PERMISSION_DENIED'
? 'PERMISSION DENIED'
: 'SCAN FAILED'}
</Text>
<Text variant="bodyLarge" style={styles.errorMessage}>
{error?.message || 'Unable to read NFC tag'}
</Text>
{getErrorHint() && (
<Text variant="bodyMedium" style={styles.hintText}>
{getErrorHint()}
</Text>
)}
<DTButton
variant="normal"
onPress={handleRetry}>
TRY AGAIN
</DTButton>
</>
)}
{state === 'idle' && (
<>
<Text variant="headlineMedium" style={styles.readyText}>
READY TO SCAN
</Text>
<DTButton
variant="normal"
onPress={startScan}>
START
</DTButton>
</>
)}
{state === 'processing' && (
<>
<ScanAnimation
isActive={true}
size={180}
color={DTColors.modeEmphasis}
/>
<Text variant="headlineMedium" style={styles.processingText}>
IDENTIFYING
</Text>
<Text variant="bodyMedium" style={styles.detectingHint}>
Analyzing chip type...
</Text>
</>
)}
</View>
<View style={styles.footer}>
<Button
mode="text"
onPress={handleCancel}
labelStyle={styles.cancelLabel}>
CANCEL
</Button>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: DTColors.dark,
padding: 24,
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
scanningText: {
color: DTColors.modeNormal,
letterSpacing: 4,
marginTop: 32,
marginBottom: 16,
},
instructionText: {
color: DTColors.light,
opacity: 0.8,
textAlign: 'center',
paddingHorizontal: 20,
},
progressText: {
color: DTColors.modeNormal,
opacity: 0.7,
marginTop: 16,
textAlign: 'center',
fontStyle: 'italic',
},
errorIcon: {
width: 80,
height: 80,
borderRadius: 40,
borderWidth: 3,
borderColor: DTColors.modeWarning,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 24,
},
errorIconText: {
color: DTColors.modeWarning,
fontSize: 48,
fontWeight: 'bold',
},
errorText: {
color: DTColors.modeWarning,
letterSpacing: 2,
marginBottom: 16,
},
warningIcon: {
width: 80,
height: 80,
borderRadius: 40,
borderWidth: 3,
borderColor: DTColors.modeEmphasis,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 24,
},
warningIconText: {
color: DTColors.modeEmphasis,
fontSize: 36,
},
warningText: {
color: DTColors.modeEmphasis,
letterSpacing: 2,
marginBottom: 16,
},
errorMessage: {
color: DTColors.light,
opacity: 0.8,
textAlign: 'center',
marginBottom: 24,
paddingHorizontal: 20,
},
hintText: {
color: DTColors.modeNormal,
opacity: 0.7,
textAlign: 'center',
marginBottom: 24,
paddingHorizontal: 20,
fontStyle: 'italic',
},
settingsButton: {
marginTop: 16,
},
readyText: {
color: DTColors.modeEmphasis,
letterSpacing: 2,
marginBottom: 32,
},
processingText: {
color: DTColors.modeEmphasis,
letterSpacing: 4,
marginTop: 32,
},
detectingHint: {
color: DTColors.light,
opacity: 0.7,
marginTop: 12,
},
footer: {
alignItems: 'center',
paddingBottom: 20,
},
cancelLabel: {
color: DTColors.light,
opacity: 0.6,
},
retryLabel: {
color: DTColors.modeNormal,
marginBottom: 8,
},
});