import {useCallback, useState} from 'react' import {Pressable, useWindowDimensions, View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' import Svg, {Path} from 'react-native-svg' import { type BarcodeScanningResult, CameraView, useCameraPermissions, } from 'expo-camera' import {useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {type NavigationProp} from '#/lib/routes/types' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow' import {CircleInfo_Stroke2_Corner0_Rounded as InfoIcon} from '#/components/icons/CircleInfo' import * as Layout from '#/components/Layout' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' const SCAN_FRAME_SIZE = 333 const SCAN_FRAME_RADIUS = 16 export function InviteScannerScreen() { const {t: l} = useLingui() const t = useTheme() const ax = useAnalytics() const insets = useSafeAreaInsets() const navigation = useNavigation() const [permission, requestPermission] = useCameraPermissions() const [showError, setShowError] = useState(false) const [scannerEnabled, setScannerEnabled] = useState(true) const onClose = useCallback(() => { navigation.goBack() }, [navigation]) const onBarcodeScanned = useCallback( (result: BarcodeScanningResult) => { if (!scannerEnabled) return const url = result.data?.trim() if (!url) return // Match bsky.app/profile/{handle} URLs (with or without https://). const profileMatch = url.match( /^(?:https?:\/\/)?bsky\.app\/profile\/([^/?#]+)/i, ) if (!profileMatch) { ax.metric('invite:scanner:scanned', {result: 'invalidQr'}) setScannerEnabled(false) setShowError(true) return } ax.metric('invite:scanner:scanned', {result: 'profileFound'}) setScannerEnabled(false) const handle = profileMatch[1] navigation.replace('Profile', {name: handle}) }, [ax, scannerEnabled, navigation], ) const onRetry = useCallback(() => { setShowError(false) setScannerEnabled(true) }, []) // Permission states --------------------------------------------------------- if (!permission) { // Still loading the initial permission state — render nothing. return } if (!permission.granted) { return ( {l`Scan QR code`} {l`Camera access needed`} {l`Bluesky needs camera access to scan QR codes from other profiles.`} ) } // Active scanner ------------------------------------------------------------ return ( {/* scan-frame outline (the colored border around the cutout) */} [ { width: 32, height: 32, alignItems: 'center', justifyContent: 'center', opacity: pressed ? 0.6 : 1, }, ]}> {/* Error overlay shown when scanned QR isn't a valid profile URL */} {showError && ( {l`Profile not found`} {l`Oops, this profile doesn't exist. Try scanning another one.`} )} ) } /** * Renders a dark scrim covering the camera view with a rounded-rect cutout in * the middle. Implemented as a single SVG Path (outer screen rect + inner * rounded rect, even-odd fill) so the cutout's corners match the colored * outline above it. */ function ScannerScrim() { const {width, height} = useWindowDimensions() const r = SCAN_FRAME_RADIUS const half = SCAN_FRAME_SIZE / 2 const x = width / 2 - half const y = height / 2 - half const right = x + SCAN_FRAME_SIZE const bottom = y + SCAN_FRAME_SIZE const d = `M0 0 H${width} V${height} H0 Z ` + `M${x + r} ${y} H${right - r} A${r} ${r} 0 0 1 ${right} ${y + r} ` + `V${bottom - r} A${r} ${r} 0 0 1 ${right - r} ${bottom} ` + `H${x + r} A${r} ${r} 0 0 1 ${x} ${bottom - r} ` + `V${y + r} A${r} ${r} 0 0 1 ${x + r} ${y} Z` return ( ) }