- 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.
171 lines
5.2 KiB
TypeScript
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,
|
|
},
|
|
});
|