feat: integrate NFC functionality for reading and writing tags
- Added NFCManagerRepository to handle NFC operations using react-native-nfc-manager. - Implemented ReadNFCTagUseCase and WriteNFCTagUseCase for reading and writing NFC tags. - Updated app layout to include NFC reader screen. - Created WriteNFCScreen for writing NFC tags with filament data. - Enhanced HomeScreen to support NFC reading and added NFC button in FilamentDetailScreen. - Updated app.json to include react-native-nfc-manager dependency. - Added animations and user feedback for NFC operations in the UI.
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Animated,
|
||||
} from 'react-native';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { SvgXml } from 'react-native-svg';
|
||||
import { colors, typography, spacing, radius } from '@shared/theme';
|
||||
import { writeNFCTagUseCase, nfcRepository } from '@infrastructure/container';
|
||||
import { Button } from '@presentation/components/ui/Button';
|
||||
|
||||
const NFC_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<g stroke="${colors.textSecondary}" stroke-width="10" fill="none">
|
||||
<path d="M 23.49 41.51 A 12 12 0 0 1 23.49 58.49"/>
|
||||
<path d="M 34.80 30.20 A 28 28 0 0 1 34.80 69.80"/>
|
||||
<path d="M 46.11 18.89 A 44 44 0 0 1 46.11 81.11"/>
|
||||
<path d="M 57.43 7.57 A 60 60 0 0 1 57.43 92.43"/>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
type Status = 'waiting' | 'writing' | 'success' | 'error';
|
||||
|
||||
/**
|
||||
* Tela de gravação NFC — acessada a partir do Detalhe do Filamento
|
||||
*/
|
||||
export default function WriteNFCScreen(): React.ReactElement {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const router = useRouter();
|
||||
const [status, setStatus] = useState<Status>('waiting');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const ring1 = useRef(new Animated.Value(0)).current;
|
||||
const ring2 = useRef(new Animated.Value(0)).current;
|
||||
const ring3 = useRef(new Animated.Value(0)).current;
|
||||
const animRef = useRef<Animated.CompositeAnimation[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
startPulse();
|
||||
startWriting();
|
||||
return () => {
|
||||
stopPulse();
|
||||
nfcRepository.cancelSession().catch(() => {});
|
||||
};
|
||||
}, []);
|
||||
|
||||
function startPulse(): void {
|
||||
const pulse = (ring: Animated.Value, delay: number) =>
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.delay(delay),
|
||||
Animated.timing(ring, { toValue: 1, duration: 1400, useNativeDriver: true }),
|
||||
Animated.timing(ring, { toValue: 0, duration: 0, useNativeDriver: true }),
|
||||
]),
|
||||
);
|
||||
const a1 = pulse(ring1, 0);
|
||||
const a2 = pulse(ring2, 400);
|
||||
const a3 = pulse(ring3, 800);
|
||||
animRef.current = [a1, a2, a3];
|
||||
a1.start(); a2.start(); a3.start();
|
||||
}
|
||||
|
||||
function stopPulse(): void {
|
||||
animRef.current.forEach((a) => a.stop());
|
||||
ring1.setValue(0); ring2.setValue(0); ring3.setValue(0);
|
||||
}
|
||||
|
||||
async function startWriting(): Promise<void> {
|
||||
setStatus('writing');
|
||||
try {
|
||||
await writeNFCTagUseCase.execute(id);
|
||||
stopPulse();
|
||||
setStatus('success');
|
||||
} catch (err: unknown) {
|
||||
stopPulse();
|
||||
setErrorMessage(err instanceof Error ? err.message : 'Erro ao gravar a tag.');
|
||||
setStatus('error');
|
||||
}
|
||||
}
|
||||
|
||||
const ringStyle = (anim: Animated.Value) => ({
|
||||
opacity: anim.interpolate({ inputRange: [0, 0.5, 1], outputRange: [0.5, 0.15, 0] }),
|
||||
transform: [{ scale: anim.interpolate({ inputRange: [0, 1], outputRange: [1, 2.2] }) }],
|
||||
});
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safe}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => {
|
||||
nfcRepository.cancelSession().catch(() => {});
|
||||
router.back();
|
||||
}}>
|
||||
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>Gravar NFC</Text>
|
||||
<View style={{ width: 24 }} />
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<View style={styles.content}>
|
||||
{/* Icon with rings */}
|
||||
<View style={styles.iconArea}>
|
||||
{status === 'writing' && (
|
||||
<>
|
||||
<Animated.View style={[styles.ring, ringStyle(ring3)]} />
|
||||
<Animated.View style={[styles.ring, ringStyle(ring2)]} />
|
||||
<Animated.View style={[styles.ring, ringStyle(ring1)]} />
|
||||
</>
|
||||
)}
|
||||
<View style={[
|
||||
styles.iconCircle,
|
||||
status === 'success' && styles.iconCircleSuccess,
|
||||
status === 'error' && styles.iconCircleError,
|
||||
]}>
|
||||
{status === 'success'
|
||||
? <Ionicons name="checkmark" size={40} color={colors.stockOk} />
|
||||
: status === 'error'
|
||||
? <Ionicons name="close" size={40} color={colors.error} />
|
||||
: <SvgXml xml={NFC_SVG} width={40} height={40} />
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Status text */}
|
||||
{status === 'waiting' || status === 'writing' ? (
|
||||
<>
|
||||
<Text style={styles.title}>Aproxime a tag NTAG215</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Encoste a tag NFC ao sensor do dispositivo para gravar os dados do filamento
|
||||
</Text>
|
||||
</>
|
||||
) : status === 'success' ? (
|
||||
<>
|
||||
<Text style={styles.title}>Tag gravada!</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
A tag NFC foi configurada com sucesso para este filamento
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.titleError}>Falha na gravação</Text>
|
||||
<Text style={styles.subtitle}>{errorMessage}</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Footer actions */}
|
||||
<View style={styles.footer}>
|
||||
{status === 'success' ? (
|
||||
<Button label="Concluir" onPress={() => router.back()} />
|
||||
) : status === 'error' ? (
|
||||
<>
|
||||
<Button label="Tentar novamente" onPress={startWriting} />
|
||||
<TouchableOpacity style={styles.cancelBtn} onPress={() => router.back()}>
|
||||
<Text style={styles.cancelText}>Cancelar</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
style={styles.cancelBtn}
|
||||
onPress={() => {
|
||||
nfcRepository.cancelSession().catch(() => {});
|
||||
router.back();
|
||||
}}
|
||||
>
|
||||
<Text style={styles.cancelText}>Cancelar</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const RING_SIZE = 120;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1, backgroundColor: colors.bgBase },
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing[5],
|
||||
paddingVertical: spacing[4],
|
||||
},
|
||||
headerTitle: {
|
||||
fontFamily: typography.fontFamily.ui,
|
||||
fontSize: typography.fontSize.md,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.textPrimary,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: spacing[4],
|
||||
paddingHorizontal: spacing[6],
|
||||
},
|
||||
iconArea: {
|
||||
width: RING_SIZE * 2.2,
|
||||
height: RING_SIZE * 2.2,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: spacing[2],
|
||||
},
|
||||
ring: {
|
||||
position: 'absolute',
|
||||
width: RING_SIZE,
|
||||
height: RING_SIZE,
|
||||
borderRadius: RING_SIZE / 2,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.accent,
|
||||
},
|
||||
iconCircle: {
|
||||
width: RING_SIZE,
|
||||
height: RING_SIZE,
|
||||
borderRadius: RING_SIZE / 2,
|
||||
backgroundColor: colors.bgHover,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
iconCircleSuccess: {
|
||||
borderColor: colors.stockOk,
|
||||
backgroundColor: `${colors.stockOk}15`,
|
||||
},
|
||||
iconCircleError: {
|
||||
borderColor: colors.error,
|
||||
backgroundColor: `${colors.error}15`,
|
||||
},
|
||||
title: {
|
||||
fontFamily: typography.fontFamily.ui,
|
||||
fontSize: typography.fontSize.xl,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.textPrimary,
|
||||
textAlign: 'center',
|
||||
},
|
||||
titleError: {
|
||||
fontFamily: typography.fontFamily.ui,
|
||||
fontSize: typography.fontSize.xl,
|
||||
fontWeight: typography.fontWeight.semibold,
|
||||
color: colors.error,
|
||||
textAlign: 'center',
|
||||
},
|
||||
subtitle: {
|
||||
fontFamily: typography.fontFamily.ui,
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.textSecondary,
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
},
|
||||
footer: {
|
||||
padding: spacing[5],
|
||||
gap: spacing[3],
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
cancelBtn: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing[3],
|
||||
},
|
||||
cancelText: {
|
||||
fontFamily: typography.fontFamily.ui,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user