- 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.
230 lines
6.1 KiB
TypeScript
230 lines
6.1 KiB
TypeScript
import React, { useEffect, useRef } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
TouchableOpacity,
|
|
StyleSheet,
|
|
Animated,
|
|
Alert,
|
|
} from 'react-native';
|
|
import { useRouter } from 'expo-router';
|
|
import { SvgXml } from 'react-native-svg';
|
|
import { colors, typography, spacing, radius } from '@shared/theme';
|
|
import { readNFCTagUseCase, nfcRepository } from '@infrastructure/container';
|
|
|
|
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>`;
|
|
|
|
/**
|
|
* Bottom sheet modal de leitura NFC — artboard 1RY-0
|
|
*/
|
|
export default function NFCReaderScreen(): React.ReactElement {
|
|
const router = useRouter();
|
|
const ring1 = useRef(new Animated.Value(0)).current;
|
|
const ring2 = useRef(new Animated.Value(0)).current;
|
|
const ring3 = useRef(new Animated.Value(0)).current;
|
|
|
|
useEffect(() => {
|
|
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);
|
|
a1.start();
|
|
a2.start();
|
|
a3.start();
|
|
|
|
startReading();
|
|
|
|
return () => {
|
|
a1.stop();
|
|
a2.stop();
|
|
a3.stop();
|
|
nfcRepository.cancelSession().catch(() => {});
|
|
};
|
|
}, []);
|
|
|
|
async function startReading(): Promise<void> {
|
|
try {
|
|
const filamentId = await readNFCTagUseCase.execute();
|
|
router.replace(`/(app)/inventory/${filamentId}` as never);
|
|
} catch (err: unknown) {
|
|
const message = err instanceof Error ? err.message : 'Erro ao ler a tag.';
|
|
Alert.alert('Erro NFC', message, [{ text: 'OK', onPress: () => router.back() }]);
|
|
}
|
|
}
|
|
|
|
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 (
|
|
<View style={styles.overlay}>
|
|
{/* Bottom sheet */}
|
|
<View style={styles.sheet}>
|
|
{/* Drag handle */}
|
|
<View style={styles.handle} />
|
|
|
|
{/* NFC animation area */}
|
|
<View style={styles.iconArea}>
|
|
<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}>
|
|
<SvgXml xml={NFC_SVG} width={40} height={40} />
|
|
</View>
|
|
</View>
|
|
|
|
{/* Status badge */}
|
|
<View style={styles.badge}>
|
|
<View style={styles.badgeDot} />
|
|
<Text style={styles.badgeText}>LENDO...</Text>
|
|
</View>
|
|
|
|
{/* Text */}
|
|
<Text style={styles.title}>Aguardando NFC</Text>
|
|
<Text style={styles.subtitle}>
|
|
Aproxime a tag do carretel ao sensor NFC do seu dispositivo
|
|
</Text>
|
|
|
|
{/* Cancel */}
|
|
<TouchableOpacity
|
|
style={styles.cancelBtn}
|
|
onPress={() => {
|
|
nfcRepository.cancelSession().catch(() => {});
|
|
router.back();
|
|
}}
|
|
>
|
|
<Text style={styles.cancelText}>Cancelar</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const RING_SIZE = 120;
|
|
|
|
const styles = StyleSheet.create({
|
|
overlay: {
|
|
flex: 1,
|
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
justifyContent: 'flex-end',
|
|
},
|
|
sheet: {
|
|
backgroundColor: colors.bgSurface,
|
|
borderTopLeftRadius: radius.xl,
|
|
borderTopRightRadius: radius.xl,
|
|
paddingBottom: spacing[10],
|
|
paddingHorizontal: spacing[6],
|
|
alignItems: 'center',
|
|
gap: spacing[4],
|
|
paddingTop: spacing[3],
|
|
},
|
|
handle: {
|
|
width: 36,
|
|
height: 4,
|
|
borderRadius: radius.full,
|
|
backgroundColor: colors.border,
|
|
marginBottom: spacing[4],
|
|
},
|
|
iconArea: {
|
|
width: RING_SIZE * 2.2,
|
|
height: RING_SIZE * 2.2,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
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',
|
|
},
|
|
badge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: spacing[2],
|
|
backgroundColor: colors.bgHover,
|
|
paddingHorizontal: spacing[3],
|
|
paddingVertical: spacing[1],
|
|
borderRadius: radius.full,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
},
|
|
badgeDot: {
|
|
width: 7,
|
|
height: 7,
|
|
borderRadius: 4,
|
|
backgroundColor: colors.stockOk,
|
|
},
|
|
badgeText: {
|
|
fontFamily: typography.fontFamily.ui,
|
|
fontSize: typography.fontSize.xs,
|
|
fontWeight: typography.fontWeight.semibold,
|
|
color: colors.textSecondary,
|
|
letterSpacing: 0.8,
|
|
},
|
|
title: {
|
|
fontFamily: typography.fontFamily.ui,
|
|
fontSize: typography.fontSize.xl,
|
|
fontWeight: typography.fontWeight.semibold,
|
|
color: colors.textPrimary,
|
|
textAlign: 'center',
|
|
},
|
|
subtitle: {
|
|
fontFamily: typography.fontFamily.ui,
|
|
fontSize: typography.fontSize.sm,
|
|
color: colors.textSecondary,
|
|
textAlign: 'center',
|
|
lineHeight: 20,
|
|
},
|
|
cancelBtn: {
|
|
marginTop: spacing[2],
|
|
backgroundColor: colors.bgHover,
|
|
borderRadius: radius.md,
|
|
paddingVertical: spacing[4],
|
|
width: '100%',
|
|
alignItems: 'center',
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
},
|
|
cancelText: {
|
|
fontFamily: typography.fontFamily.ui,
|
|
fontSize: typography.fontSize.base,
|
|
color: colors.textPrimary,
|
|
},
|
|
});
|