Files
MeowSpool/mobile/app/(app)/scanner.tsx
T
Felipe 416e13893c feat: implement authentication flow and session management
- Update RootLayout to handle authentication state and redirect users based on their auth status.
- Create an Index component to redirect unauthenticated users to the login page.
- Modify ApiAuthRepository to fetch user session data using access and refresh tokens.
- Introduce a new container file to manage use case instances for authentication and filament operations.
- Enhance authStore to validate tokens and fetch user data from the API.
- Add babel-plugin-module-resolver for improved module imports.
- Update package.json scripts for running the app on Android and iOS.
- Add expo-camera dependency for camera functionalities.
- Update tsconfig.json to include infrastructure path mapping.
2026-03-14 13:22:17 -03:00

171 lines
5.2 KiB
TypeScript

import React, { useRef } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { CameraView, useCameraPermissions } from 'expo-camera';
import { colors, typography, spacing } from '@shared/theme';
/**
* Tela de scanner de QR Code
* Lê deeplinks meowspool://filament/<id> e navega para o filamento.
*/
export default function ScannerScreen(): React.ReactElement {
const router = useRouter();
const [permission, requestPermission] = useCameraPermissions();
const handledRef = useRef(false);
function handleBarCodeScanned({ data }: { data: string }): void {
if (handledRef.current) return;
handledRef.current = true;
const match = data.match(/meowspool:\/\/filament\/([^/]+)/);
if (match) {
router.replace(`/(app)/inventory/${match[1]}` as never);
} else {
// QR não reconhecido — libera para próximo scan
handledRef.current = false;
}
}
if (!permission) {
return (
<SafeAreaView style={styles.safe}>
<View style={styles.center}>
<Text style={styles.message}>Carregando câmera</Text>
</View>
</SafeAreaView>
);
}
if (!permission.granted) {
return (
<SafeAreaView style={styles.safe}>
<View style={styles.center}>
<Ionicons name="camera-outline" size={48} color={colors.textSecondary} />
<Text style={styles.message}>Permissão de câmera necessária</Text>
<TouchableOpacity style={styles.permBtn} onPress={requestPermission}>
<Text style={styles.permBtnText}>Conceder permissão</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.safe}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()}>
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
</TouchableOpacity>
<Text style={styles.headerTitle}>Escanear QR Code</Text>
<View style={{ width: 24 }} />
</View>
{/* Camera */}
<View style={styles.cameraWrapper}>
<CameraView
style={StyleSheet.absoluteFillObject}
facing="back"
mode="picture"
barcodeScannerSettings={{ barcodeTypes: ['qr'] }}
onBarcodeScanned={handleBarCodeScanned}
/>
{/* Viewfinder overlay */}
<View style={styles.overlay}>
<View style={styles.viewfinder}>
<View style={[styles.corner, styles.cornerTL]} />
<View style={[styles.corner, styles.cornerTR]} />
<View style={[styles.corner, styles.cornerBL]} />
<View style={[styles.corner, styles.cornerBR]} />
</View>
</View>
</View>
<View style={styles.hint}>
<Text style={styles.hintText}>
Aponte para o QR Code do filamento
</Text>
</View>
</SafeAreaView>
);
}
const CORNER = 24;
const CORNER_THICKNESS = 3;
const VIEWFINDER_SIZE = 240;
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: colors.bgBase },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: spacing[4], paddingHorizontal: spacing[6] },
message: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.base,
color: colors.textSecondary,
textAlign: 'center',
},
permBtn: {
backgroundColor: colors.accent,
paddingHorizontal: spacing[5],
paddingVertical: spacing[3],
borderRadius: 8,
},
permBtnText: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.base,
fontWeight: '600',
color: 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: '600',
color: colors.textPrimary,
},
cameraWrapper: {
flex: 1,
},
overlay: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(0,0,0,0.55)',
},
viewfinder: {
width: VIEWFINDER_SIZE,
height: VIEWFINDER_SIZE,
backgroundColor: 'transparent',
},
corner: {
position: 'absolute',
width: CORNER,
height: CORNER,
borderColor: colors.accent,
},
cornerTL: { top: 0, left: 0, borderTopWidth: CORNER_THICKNESS, borderLeftWidth: CORNER_THICKNESS },
cornerTR: { top: 0, right: 0, borderTopWidth: CORNER_THICKNESS, borderRightWidth: CORNER_THICKNESS },
cornerBL: { bottom: 0, left: 0, borderBottomWidth: CORNER_THICKNESS, borderLeftWidth: CORNER_THICKNESS },
cornerBR: { bottom: 0, right: 0, borderBottomWidth: CORNER_THICKNESS, borderRightWidth: CORNER_THICKNESS },
hint: {
paddingVertical: spacing[5],
paddingHorizontal: spacing[5],
alignItems: 'center',
borderTopWidth: 1,
borderTopColor: colors.border,
},
hintText: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
},
});