74 lines
2.5 KiB
TypeScript
74 lines
2.5 KiB
TypeScript
/**
|
|
* #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<string>(`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 }
|
|
}
|