@@ -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,76 @@ 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 = Required < Options > &
51+ Omit <
52+ typeof ReactScanInternals . options ,
53+ 'playSound' | 'runInProduction' | 'includeChildren'
54+ > ;
55+
56+ const OptionsContext = createContext < ReactNativeScanOptions > ( {
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 = { options } >
92+ { ! isPaused && < ReactScanCanvas scanTag = "react-scan-no-traverse" /> }
93+ { options . showToolbar && (
94+ < ReactScanToolbar
95+ scanTag = "react-scan-no-traverse"
96+ isPaused = { isPaused }
97+ />
98+ ) }
99+ </ OptionsContext . Provider >
100+ </ >
101+ ) ;
102+ } ;
103+
104+ const ReactScanToolbar = ( {
105+ isPaused,
106+ } : {
107+ isPaused : boolean ;
108+ scanTag : string ;
109+ } ) => {
51110 const pan = useRef ( new Animated . ValueXY ( ) ) . current ;
52111
53112 const panResponder = useRef (
@@ -70,77 +129,57 @@ export const ReactNativeScanEntryPoint = () => {
70129 } ) ,
71130 ) . current ;
72131
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-
89132 return (
90- < >
91- { ! isPaused && < ReactNativeScan id = "react-scan-no-traverse" /> }
92-
93- < Animated . View
94- id = "react-scan-no-traverse"
133+ < Animated . View
134+ id = "react-scan-no-traverse"
135+ style = { {
136+ position : 'absolute' ,
137+ bottom : 20 ,
138+ right : 20 ,
139+ zIndex : 999999 ,
140+ transform : pan . getTranslateTransform ( ) ,
141+ } }
142+ { ...panResponder . panHandlers }
143+ >
144+ < Pressable
145+ onPress = { ( ) =>
146+ ( ReactScanInternals . isPaused = ! ReactScanInternals . isPaused )
147+ }
95148 style = { {
96- position : 'absolute' ,
97- bottom : 20 ,
98- right : 20 ,
99- zIndex : 999999 ,
100- transform : pan . getTranslateTransform ( ) ,
149+ backgroundColor : ! isPaused
150+ ? 'rgba(88, 82, 185, 0.9)'
151+ : 'rgba(88, 82, 185, 0.5)' ,
152+ paddingHorizontal : 12 ,
153+ paddingVertical : 6 ,
154+ borderRadius : 4 ,
155+ flexDirection : 'row' ,
156+ alignItems : 'center' ,
157+ gap : 8 ,
101158 } }
102- { ...panResponder . panHandlers }
103159 >
104- < Pressable
105- onPress = { ( ) =>
106- ( ReactScanInternals . isPaused = ! ReactScanInternals . isPaused )
107- }
160+ < View
108161 style = { {
109- backgroundColor : ! isPaused
110- ? 'rgba(88, 82, 185, 0.9)'
111- : 'rgba(88, 82, 185, 0.5)' ,
112- paddingHorizontal : 12 ,
113- paddingVertical : 6 ,
162+ width : 8 ,
163+ height : 8 ,
114164 borderRadius : 4 ,
115- flexDirection : 'row' ,
116- alignItems : 'center' ,
117- gap : 8 ,
165+ backgroundColor : ! isPaused ? '#4ADE80' : '#666' ,
166+ } }
167+ />
168+ < RNText
169+ style = { {
170+ color : 'white' ,
171+ fontSize : 14 ,
172+ fontWeight : 'bold' ,
173+ fontFamily : Platform . select ( {
174+ ios : 'Courier' ,
175+ default : 'monospace' ,
176+ } ) ,
118177 } }
119178 >
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- </ >
179+ React Scan
180+ </ RNText >
181+ </ Pressable >
182+ </ Animated . View >
144183 ) ;
145184} ;
146185const dimensions = Dimensions . get ( 'window' ) ;
@@ -155,24 +194,30 @@ const font = matchFont({
155194const getTextWidth = ( text : string ) => {
156195 return ( text || 'unknown' ) . length * 7 ;
157196} ;
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);
175197
198+ const useOutlines = ( opacity : { value : number } ) => {
199+ const [ outlines , setOutlines ] = useState <
200+ ( typeof ReactScanInternals ) [ 'activeOutlines' ]
201+ > ( [ ] ) ;
202+ const options = useContext ( OptionsContext ) ;
203+ // cannot use useSyncExternalStore for back compat
204+ useEffect ( ( ) => {
205+ ReactScanInternals . subscribe ( 'activeOutlines' , ( activeOutlines ) => {
206+ setOutlines ( activeOutlines ) ;
207+ if ( options . animationWhenFlashing !== false ) {
208+ // we only support fade-out for now
209+ opacity . value = 1 ;
210+ opacity . value = withTiming ( 0 , {
211+ duration : 500 ,
212+ } ) ;
213+ }
214+ } ) ;
215+ } , [ ] ) ;
216+ return outlines ;
217+ } ;
218+ const ReactScanCanvas = ( _ : { scanTag : string } ) => {
219+ const opacity = useSharedValue ( 1 ) ;
220+ const outlines = useOutlines ( opacity ) ;
176221 return (
177222 < Canvas
178223 style = { {
@@ -255,8 +300,3 @@ const ReactNativeScan = ({ id: _ }: { id: string }) => {
255300 </ Canvas >
256301 ) ;
257302} ;
258-
259-
260-
261-
262-
0 commit comments