import Ionicons from '@expo/vector-icons/build/Ionicons'; import Slider from '@react-native-community/slider'; import * as BarCodeScanner from 'expo-barcode-scanner'; import { BlurView } from 'expo-blur'; import { Camera, CameraType, FlashMode } from 'expo-camera'; import * as Haptics from 'expo-haptics'; import * as React from 'react'; import { Animated, Easing, Platform, Pressable, StyleProp, StyleSheet, Text, TouchableOpacity, View, ViewStyle, } from 'react-native'; import { Path, Svg, SvgProps } from 'react-native-svg'; import Colors from '../constants/Colors'; import usePermissions from '../utilities/usePermissions'; function useCameraTypes(): CameraType[] | null { const [types, setTypes] = React.useState(null); React.useEffect(() => { let isMounted = true; if (Platform.OS !== 'web') { setTypes([CameraType.front, CameraType.back]); } else { // TODO: This method isn't supported on native Camera.getAvailableCameraTypesAsync().then((types) => { if (isMounted) { setTypes(types as CameraType[]); } }); } return () => { isMounted = false; }; }, []); return types; } function useToggleCameraType(preferredInitialType: CameraType): { // The current camera type, null when loading types. type: CameraType | null; // Available camera types, null when loading types. types: CameraType[] | null; // Toggle the current camera type to the next available camera type, null when toggling isn't possible (1 or less cameras on the device). toggle: null | (() => CameraType); } { const [type, setType] = React.useState(null); const types = useCameraTypes(); React.useEffect(() => { if (!types) return; if (types.includes(preferredInitialType)) { setType(preferredInitialType); } else { setType(types[0]); } }, [types]); const toggle = types && types.length > 1 ? () => { const selectedIndex = types.findIndex((c) => c === type); const nextIndex = (selectedIndex + 1) % types.length; setType(types[nextIndex]); return types[nextIndex]; } : null; return { type, toggle, types }; } function useCameraAvailable(): boolean { const [isAvailable, setAvailable] = React.useState(false); React.useEffect(() => { let isMounted = true; if (Platform.OS !== 'web') { setAvailable(true); } else { // TODO: This method isn't supported on native Camera.isAvailableAsync().then((isAvailable) => { if (isMounted) { setAvailable(isAvailable); } }); } return () => { isMounted = false; }; }, []); return isAvailable; } export default function QRCodeScreen() { const [isPermissionsGranted] = usePermissions(Camera.requestCameraPermissionsAsync); const isAvailable = useCameraAvailable(); if (!isPermissionsGranted || !isAvailable) { // this can also occur if the device doesn't have a camera const message = isAvailable ? 'You have not granted permission to use the camera on this device!' : 'Your device does not have a camera'; return ( {message} ); } return ; } QRCodeScreen.navigationOptions = { title: 'QR Code', }; function QRCodeView() { const [data, setData] = React.useState(null); const [isLit, setLit] = React.useState(false); const [zoom, setZoom] = React.useState(0); const { type, toggle } = useToggleCameraType(CameraType.back); const onFlashToggle = React.useCallback(() => { setLit((isLit) => !isLit); }, []); // hide footer when no actions are possible -- i.e. desktop web const showFooter = !!toggle || type === CameraType.back; // TODO(Bacon): We need a way to determine if the current camera supports certain capabilities (like zooming). const supportsZoom = true; return ( {type && ( ( )}> { if (data !== incoming.data) { console.log('found: ', incoming); setData(incoming.data); } }} style={{ flex: 1 }} flashMode={isLit ? FlashMode.torch : FlashMode.off} /> )} {data && {data}} {showFooter && ( )} ); } function OverlayView({ style, renderOverlay, ...props }: React.ComponentProps & { renderOverlay: () => React.ReactNode; children?: React.ReactNode; }) { const [isOverlayActive, setOverlayActive] = React.useState(false); const timer = React.useRef(); const opacity = React.useRef(new Animated.Value(0)); React.useEffect(() => { Animated.timing(opacity.current, { toValue: isOverlayActive ? 1 : 0, duration: 500, useNativeDriver: true, }).start(); }, [isOverlayActive]); const onPress = () => { clearTimeout(timer.current); setOverlayActive(true); // @ts-expect-error: TS resolves node types first timer.current = setTimeout(() => { setOverlayActive(() => false); }, 5000); }; return ( {props.children} {renderOverlay()} ); } function Hint({ children }: { children: string }) { return ( {children} ); } function QRIndicator() { const scale = React.useMemo(() => new Animated.Value(1), []); const duration = 500; React.useEffect(() => { let mounted = true; function cycleAnimation() { Animated.sequence([ Animated.timing(scale, { easing: Easing.in(Easing.quad), toValue: 1, duration, useNativeDriver: true, }), Animated.timing(scale, { easing: Easing.out(Easing.quad), toValue: 1.05, duration, useNativeDriver: true, }), ]).start(() => { if (mounted) { cycleAnimation(); } }); } cycleAnimation(); return () => { mounted = false; }; }, []); return ( ); } class SvgComponent extends React.Component { render() { const props = { ...this.props }; if (Platform.OS === 'web') { delete props.collapsable; } return ( ); } } const AnimatedScanner = Animated.createAnimatedComponent(SvgComponent); // note(bacon): Purposefully skip using the themed icons since we want the icons to change color based on toggle state. const shouldUseHaptics = Platform.OS === 'ios'; const size = 64; const slop = 40; const hitSlop = { top: slop, bottom: slop, right: slop, left: slop }; function QRFooterButton({ onPress, isActive = false, iconName, iconSize = 36, style, disabled, }: { style?: StyleProp; onPress?: (() => void) | null; isActive?: boolean; iconName: React.ComponentProps['name']; iconSize?: number; disabled?: boolean; }) { const tint = isActive ? 'default' : 'dark'; const iconColor = isActive ? Colors.tintColor : '#ffffff'; const onPressIn = React.useCallback(() => { if (shouldUseHaptics) Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); }, []); const onPressButton = React.useCallback(() => { onPress?.(); if (shouldUseHaptics) Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); }, [onPress]); return ( ); } const styles = StyleSheet.create({ buttonContainer: { width: size, height: size, borderRadius: size / 2, overflow: 'hidden', justifyContent: 'center', alignItems: 'center', }, scanner: { shadowColor: '#000', shadowOffset: { width: 0, height: 1, }, shadowOpacity: 0.22, shadowRadius: 2.22, }, container: { flex: 1, backgroundColor: '#fff', justifyContent: 'center', alignItems: 'center', }, hint: { paddingHorizontal: 16, paddingVertical: 20, borderRadius: 16, justifyContent: 'center', alignItems: 'center', }, header: { position: 'absolute', left: 0, right: 0, alignItems: 'center', }, headerText: { color: '#fff', backgroundColor: 'transparent', textAlign: 'center', fontSize: 16, fontWeight: '500', }, footer: { position: 'absolute', left: 0, right: 0, alignItems: 'center', flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: '10%', }, });