/** * #35 민감 화면 스크린샷 차단 훅 * * 완전 차단(Android FLAG_SECURE / iOS)은 네이티브 모듈(expo-screen-capture)이 필요하다. * 해당 모듈이 설치되어 있으면 동적으로 활성화하고, 없으면(EAS 안전 모드) * 오버레이 가림(SecureOverlay)으로 대체할 수 있도록 상태를 반환한다. * * 추가 네이티브 의존성 없이 동작 — package.json/app.json 변경 불필요. */ import { useEffect, useRef, useState } from 'react' import { AppState, AppStateStatus } from 'react-native' interface ScreenshotBlockResult { /** 앱이 비활성(백그라운드/전환 중) 상태일 때 true → 민감 내용을 오버레이로 가린다 */ shouldObscure: boolean /** 네이티브 FLAG_SECURE가 실제 적용되었는지 */ nativeSecured: boolean } let cachedModule: any | undefined // undefined: 미탐지, null: 없음, obj: 모듈 function loadScreenCaptureModule(): any | null { if (cachedModule !== undefined) return cachedModule try { // 설치되어 있을 때만 로드 (미설치 시 require가 throw → catch) // eslint-disable-next-line @typescript-eslint/no-var-requires cachedModule = require('expo-screen-capture') } catch { cachedModule = null } return cachedModule } export function useScreenshotBlock(enabled: boolean): ScreenshotBlockResult { const [shouldObscure, setShouldObscure] = useState(false) const [nativeSecured, setNativeSecured] = useState(false) const tagRef = useRef(`secure-${Math.random().toString(36).slice(2)}`) // 네이티브 FLAG_SECURE 시도 useEffect(() => { if (!enabled) return const mod = loadScreenCaptureModule() let active = true if (mod?.preventScreenCaptureAsync) { mod .preventScreenCaptureAsync(tagRef.current) .then(() => active && setNativeSecured(true)) .catch(() => active && setNativeSecured(false)) } return () => { active = false if (mod?.allowScreenCaptureAsync) { mod.allowScreenCaptureAsync(tagRef.current).catch(() => {}) } setNativeSecured(false) } }, [enabled]) // 오버레이 가림: 앱이 inactive/background 일 때 (앱 스위처 미리보기 차단) useEffect(() => { if (!enabled) { setShouldObscure(false) return } const onChange = (state: AppStateStatus) => { setShouldObscure(state !== 'active') } const sub = AppState.addEventListener('change', onChange) return () => sub.remove() }, [enabled]) return { shouldObscure, nativeSecured } }