@@ -15,16 +15,17 @@ import {
1515 Text ,
1616} from '@shopify/react-native-skia' ;
1717import React , {
18+ createContext ,
19+ useContext ,
1820 useEffect ,
1921 useRef ,
2022 useState ,
21- useSyncExternalStore ,
2223} from 'react' ;
23- // import { useDerivedValue, useSharedValue } from 'react-native-reanimated';
2424import { ReactScanInternals } from '..' ;
25+ import { useSharedValue , withTiming } from 'react-native-reanimated' ;
2526import { assertNative , instrumentNative } from './instrument' ;
2627
27- // can't use useSyncExternalStore for compat
28+ // can't use useSyncExternalStore for back compat
2829const useIsPaused = ( ) => {
2930 const [ isPaused , setIsPaused ] = useState ( ReactScanInternals . isPaused ) ;
3031 useEffect ( ( ) => {
@@ -36,18 +37,79 @@ const useIsPaused = () => {
3637 return isPaused ;
3738} ;
3839
39- export const ReactNativeScanEntryPoint = ( ) => {
40- if ( ReactScanInternals . isProd ) {
41- return null ; // todo: better no-op
42- }
40+ interface Options {
41+ /**
42+ * Controls the animation of the re-render overlay.
43+ * When set to "fade-out", the overlay will fade out after appearing.
44+ * When false, no animation will be applied.
45+ * Note: Enabling animations may impact performance.
46+ * @default false */
47+ animationWhenFlashing ?: 'fade-out' | false ;
48+ }
4349
50+ export type ReactNativeScanOptions = Options &
51+ Omit <
52+ typeof ReactScanInternals . options ,
53+ 'playSound' | 'runInProduction' | 'includeChildren'
54+ > ;
55+
56+ const OptionsContext = createContext < ReactNativeScanOptions & Required < Options > > ( {
57+ animationWhenFlashing : false ,
58+ } ) ;
59+
60+ export const ReactScan = ( {
61+ children,
62+ ...options
63+ } : {
64+ children : React . ReactNode ;
65+ } & ReactNativeScanOptions ) => {
4466 useEffect ( ( ) => {
45- if ( ! ReactScanInternals . isProd ) {
46- instrumentNative ( ) ; // cleanup?
47- }
67+ ReactScanInternals . options = options ;
68+ instrumentNative ( ) ;
4869 } , [ ] ) ;
4970 const isPaused = useIsPaused ( ) ;
5071
72+ useEffect ( ( ) => {
73+ const interval = setInterval ( ( ) => {
74+ if ( isPaused ) return ;
75+
76+ const newActive = ReactScanInternals . activeOutlines . filter (
77+ ( x ) => Date . now ( ) - x . updatedAt < 500 ,
78+ ) ;
79+ if ( newActive . length !== ReactScanInternals . activeOutlines . length ) {
80+ ReactScanInternals . set ( 'activeOutlines' , newActive ) ;
81+ }
82+ } , 200 ) ;
83+ return ( ) => {
84+ clearInterval ( interval ) ;
85+ } ;
86+ } , [ isPaused ] ) ;
87+
88+ return (
89+ < >
90+ { children }
91+ < OptionsContext . Provider value = { {
92+ ...options ,
93+ animationWhenFlashing : options . animationWhenFlashing ?? false
94+ } } >
95+ { ! isPaused && < ReactScanCanvas scanTag = "react-scan-no-traverse" /> }
96+ { options . showToolbar && (
97+ < ReactScanToolbar
98+ scanTag = "react-scan-no-traverse"
99+ isPaused = { isPaused }
100+ />
101+ ) }
102+ </ OptionsContext . Provider >
103+ </ >
104+ ) ;
105+ } ;
106+
107+ const ReactScanToolbar = ( {
108+ isPaused,
109+ } : {
110+ isPaused : boolean ;
111+ scanTag : string ;
112+ } ) => {
51113 const pan = useRef ( new Animated . ValueXY ( ) ) . current ;
52114
53115 const panResponder = useRef (
@@ -70,77 +132,57 @@ export const ReactNativeScanEntryPoint = () => {
70132 } ) ,
71133 ) . current ;
72134
73- useEffect ( ( ) => {
74- const interval = setInterval ( ( ) => {
75- if ( isPaused ) return ;
76-
77- const newActive = ReactScanInternals . activeOutlines . filter (
78- ( x ) => Date . now ( ) - x . updatedAt < 500 ,
79- ) ;
80- if ( newActive . length !== ReactScanInternals . activeOutlines . length ) {
81- ReactScanInternals . set ( 'activeOutlines' , newActive ) ;
82- }
83- } , 200 ) ;
84- return ( ) => {
85- clearInterval ( interval ) ;
86- } ;
87- } , [ isPaused ] ) ;
88-
89135 return (
90- < >
91- { ! isPaused && < ReactNativeScan id = "react-scan-no-traverse" /> }
92-
93- < Animated . View
94- id = "react-scan-no-traverse"
136+ < Animated . View
137+ id = "react-scan-no-traverse"
138+ style = { {
139+ position : 'absolute' ,
140+ bottom : 20 ,
141+ right : 20 ,
142+ zIndex : 999999 ,
143+ transform : pan . getTranslateTransform ( ) ,
144+ } }
145+ { ...panResponder . panHandlers }
146+ >
147+ < Pressable
148+ onPress = { ( ) =>
149+ ( ReactScanInternals . isPaused = ! ReactScanInternals . isPaused )
150+ }
95151 style = { {
96- position : 'absolute' ,
97- bottom : 20 ,
98- right : 20 ,
99- zIndex : 999999 ,
100- transform : pan . getTranslateTransform ( ) ,
152+ backgroundColor : ! isPaused
153+ ? 'rgba(88, 82, 185, 0.9)'
154+ : 'rgba(88, 82, 185, 0.5)' ,
155+ paddingHorizontal : 12 ,
156+ paddingVertical : 6 ,
157+ borderRadius : 4 ,
158+ flexDirection : 'row' ,
159+ alignItems : 'center' ,
160+ gap : 8 ,
101161 } }
102- { ...panResponder . panHandlers }
103162 >
104- < Pressable
105- onPress = { ( ) =>
106- ( ReactScanInternals . isPaused = ! ReactScanInternals . isPaused )
107- }
163+ < View
108164 style = { {
109- backgroundColor : ! isPaused
110- ? 'rgba(88, 82, 185, 0.9)'
111- : 'rgba(88, 82, 185, 0.5)' ,
112- paddingHorizontal : 12 ,
113- paddingVertical : 6 ,
165+ width : 8 ,
166+ height : 8 ,
114167 borderRadius : 4 ,
115- flexDirection : 'row' ,
116- alignItems : 'center' ,
117- gap : 8 ,
168+ backgroundColor : ! isPaused ? '#4ADE80' : '#666' ,
169+ } }
170+ />
171+ < RNText
172+ style = { {
173+ color : 'white' ,
174+ fontSize : 14 ,
175+ fontWeight : 'bold' ,
176+ fontFamily : Platform . select ( {
177+ ios : 'Courier' ,
178+ default : 'monospace' ,
179+ } ) ,
118180 } }
119181 >
120- < View
121- style = { {
122- width : 8 ,
123- height : 8 ,
124- borderRadius : 4 ,
125- backgroundColor : ! isPaused ? '#4ADE80' : '#666' ,
126- } }
127- />
128- < RNText
129- style = { {
130- color : 'white' ,
131- fontSize : 14 ,
132- fontWeight : 'bold' ,
133- fontFamily : Platform . select ( {
134- ios : 'Courier' ,
135- default : 'monospace' ,
136- } ) ,
137- } }
138- >
139- React Scan
140- </ RNText >
141- </ Pressable >
142- </ Animated . View >
143- </ >
182+ React Scan
183+ </ RNText >
184+ </ Pressable >
185+ </ Animated . View >
144186 ) ;
145187} ;
146188const dimensions = Dimensions . get ( 'window' ) ;
@@ -155,24 +197,30 @@ const font = matchFont({
155197const getTextWidth = ( text : string ) => {
156198 return ( text || 'unknown' ) . length * 7 ;
157199} ;
158- const ReactNativeScan = ( { id : _ } : { id : string } ) => {
159- // const opacity = useSharedValue(1);
160- // todo: polly fill
161- const outlines = useSyncExternalStore (
162- ( listener ) =>
163- ReactScanInternals . subscribe ( 'activeOutlines' , ( _ ) => {
164- // animations destroy UI thread on heavy updates, probably not worth it
165- // opacity.value = 1;
166- // opacity.value = withTiming(0, {
167- // duration: 500
168- // })
169- listener ( ) ;
170- } ) ,
171- ( ) => ReactScanInternals . activeOutlines ,
172- ) ;
173- // );
174- // const animatedOpacity = useDerivedValue(() => opacity.value);
175200
201+ const useOutlines = ( opacity : { value : number } ) => {
202+ const [ outlines , setOutlines ] = useState <
203+ ( typeof ReactScanInternals ) [ 'activeOutlines' ]
204+ > ( [ ] ) ;
205+ const options = useContext ( OptionsContext ) ;
206+ // cannot use useSyncExternalStore for back compat
207+ useEffect ( ( ) => {
208+ ReactScanInternals . subscribe ( 'activeOutlines' , ( activeOutlines ) => {
209+ setOutlines ( activeOutlines ) ;
210+ if ( options . animationWhenFlashing !== false ) {
211+ // we only support fade-out for now
212+ opacity . value = 1 ;
213+ opacity . value = withTiming ( 0 , {
214+ duration : 500 ,
215+ } ) ;
216+ }
217+ } ) ;
218+ } , [ ] ) ;
219+ return outlines ;
220+ } ;
221+ const ReactScanCanvas = ( _ : { scanTag : string } ) => {
222+ const opacity = useSharedValue ( 1 ) ;
223+ const outlines = useOutlines ( opacity ) ;
176224 return (
177225 < Canvas
178226 style = { {
@@ -255,8 +303,3 @@ const ReactNativeScan = ({ id: _ }: { id: string }) => {
255303 </ Canvas >
256304 ) ;
257305} ;
258-
259-
260-
261-
262-
0 commit comments