import React, { useContext, useEffect, useLayoutEffect, useRef } from 'react'; import { Alert, InteractionManager } from 'react-native'; import { useHeaderHeight } from '@react-navigation/elements'; import { ScreenTrackingParams } from './types'; import Navigation from './Navigation'; import { ApplicationContext, MiniAppContext, ScreenContext } from '../Context'; import { GridSystem } from '../Layout'; import { version } from '../package.json'; import { useAppState } from './utils'; import { TooltipPortalHost, TooltipPortalProvider } from './TooltipPortal'; const runAfterInteractions = InteractionManager.runAfterInteractions; /** * container for stack screen * @param props * @constructor */ const StackScreen: React.FC = props => { const { showGrid, navigator } = useContext(ApplicationContext); const tracking = useRef({ mounted: false, timeoutLoad: undefined, timeoutInteraction: undefined, timeoutTracking: undefined, timeoutLoading: undefined, startTime: Date.now(), endTime: Date.now(), traceIdLoad: undefined, traceIdInteraction: undefined, releaseLoad: undefined, releaseInteraction: undefined, releaseUserInteraction: undefined, releaseLoading: false, timeLoad: 0, timeInteraction: 0, timeStartLoading: 0, timeEndLoading: 0, widgets: [], params: undefined, lastElement: undefined, }); const widgets = useRef([]); const context = useContext(MiniAppContext); const { screen: Component, options, initialParams, bottomTab, } = props.route.params; const navigation = useRef(new Navigation(props.navigation, context)).current; const heightHeader = useHeaderHeight(); const { isBackgroundToForeground } = useAppState(); const data = { ...initialParams, ...props.route.params, navigation, }; delete data.screen; delete data.initialParams; const screenName = Component?.name || Component?.type?.name || 'Invalid'; /** * set options for screen */ useLayoutEffect(() => { if (options) { navigation.setOptions(options); } }, [navigation, options]); /** * tracking for screen */ useEffect(() => { let focusScreen: any; let onFocusApp: any; if (['Invalid', 'screen'].includes(screenName)) { navigator?.maxApi?.showPopup?.('notice', { title: 'Invalid screen name', message: 'Your screen has not been rendered because Platform has not detected the screen name. Please migrate to support this feature.', }); } const startTime = Date.now(); if (!bottomTab) { focusScreen = props.navigation.addListener('focus', () => { navigator?.maxApi?.getDataObserver?.('current_screen', (item: any) => { onScreenNavigated(item?.screenName, screenName); navigator?.maxApi?.setDataObserver?.('current_screen', { screenName, startTime, }); }); }); onFocusApp = navigator?.maxApi?.listen?.('onFocusApp', () => { if (props.navigation.isFocused()) { navigator?.maxApi?.getDataObserver?.( 'current_screen', (item: any) => { onScreenNavigated(item?.screenName, screenName); navigator?.maxApi?.setDataObserver?.('current_screen', { screenName, startTime, }); }, ); } }); } navigator?.maxApi?.startTraceScreenLoad?.( screenName, context, (item: any) => { tracking.current.traceIdLoad = item?.traceId; }, ); navigator?.maxApi?.startTraceScreenInteraction?.( screenName, context, (item: any) => { tracking.current.traceIdInteraction = item?.traceId; }, ); tracking.current.timeoutTracking = setTimeout(() => { onScreenLoad(); onScreenInteraction(); }, 5000); return () => { onScreenLoad(); onScreenInteraction(); clearTimeout(tracking.current.timeoutLoad); clearTimeout(tracking.current.timeoutInteraction); clearTimeout(tracking.current.timeoutTracking); // eslint-disable-next-line react-hooks/exhaustive-deps clearTimeout(tracking.current.timeoutLoading); focusScreen?.(); onFocusApp?.remove?.(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const onScreenNavigated = (pre: string, current: string) => { if (!isBackgroundToForeground) { const item: any = { preScreenName: pre, screenName: current, componentName: 'Screen', state: 'navigated', action: tracking.current?.mounted ? 'back' : 'push', }; context?.autoTracking?.({ ...context, ...item, }); tracking.current.mounted = true; /** * debug toast */ navigator?.maxApi?.showToastDebug?.({ appId: context.appId, message: `${screenName} screen_navigated`, type: 'ERROR', }); } }; /** * tracking for screen load */ const onScreenLoad = () => { if (!tracking.current?.releaseLoad) { let timeLoad = tracking.current.timeLoad; if (timeLoad === 0) { timeLoad = tracking.current.endTime - tracking.current.startTime; } context?.autoTracking?.({ ...context, screenName, componentName: 'Screen', state: 'load', duration: timeLoad, widgets: tracking.current.widgets, params: tracking.current.params, version: version, lastElement: tracking.current.lastElement, }); navigator?.maxApi?.stopTrace?.( tracking.current.traceIdLoad, { value: timeLoad / 1000 }, null, ); tracking.current.releaseLoad = true; /** * debug */ navigator?.maxApi?.showToastDebug?.({ appId: context.appId, message: `${screenName} screen_load_time ${timeLoad}`, type: 'ERROR', }); if ( __DEV__ && tracking.current.lastElement?.children?.current?.length > 0 ) { Alert.alert( `${screenName}- load ${timeLoad}ms`, JSON.stringify(tracking.current.lastElement?.children?.current), ); } } }; /** * tracking for screen load */ const onScreenInteraction = () => { if (!tracking.current?.releaseInteraction) { let timeLoad = tracking.current.timeLoad; if (timeLoad === 0) { timeLoad = tracking.current.endTime - tracking.current.startTime; } if (tracking.current.timeInteraction === 0) { tracking.current.timeInteraction = timeLoad; } context?.autoTracking?.({ ...context, screenName, componentName: 'Screen', state: 'interaction', duration: tracking.current.timeInteraction - timeLoad, totalDuration: tracking.current.timeInteraction, params: tracking.current.params, version: version, }); navigator?.maxApi?.stopTrace?.( tracking.current.traceIdInteraction, { value: tracking.current.timeInteraction / 1000 }, null, ); tracking.current.releaseInteraction = true; /** * debug toast */ navigator?.maxApi?.showToastDebug?.({ appId: context.appId, message: `${screenName} screen_interaction_time ${tracking.current.timeInteraction}`, type: 'ERROR', }); } }; /** * tracking for first interaction by user */ const onFirstInteraction = (action: string) => { if (!tracking.current?.releaseUserInteraction) { const timeLoad = tracking.current.endTime - tracking.current.startTime; context?.autoTracking?.({ ...context, screenName, componentName: 'Screen', state: 'interaction', duration: timeLoad, action, }); tracking.current.releaseUserInteraction = true; /** * debug */ navigator?.maxApi?.showToastDebug?.({ appId: context.appId, message: `${screenName} user_interaction ${timeLoad}`, type: 'ERROR', }); } }; /** * tracking for loading screen */ const onScreenLoading = () => { const start = tracking.current.timeStartLoading; const end = tracking.current.timeEndLoading; if (!tracking.current?.releaseLoading && start && end && end > start) { context?.autoTracking?.({ ...context, screenName, componentName: 'Screen', state: 'loading', duration: end - start, loadingType: 'skeleton', }); tracking.current.releaseLoading = true; } }; return ( { /** * widget handle */ if (item?.componentName === 'Widget') { const index = widgets.current.findIndex( (element: any) => element.componentId === item.componentId, ); const time = Date.now() - tracking.current.startTime; if (index === -1) { widgets.current.push({ componentId: item.componentId, appId: item.params?.appId, code: item.params?.code, start: time, }); } else { const exist = widgets.current[index]; widgets.current[index] = { ...exist, end: time, duration: time - exist.start, }; } return; } /** * tracking for element screen load */ clearTimeout(tracking.current.timeoutLoad); tracking.current.endTime = Date.now(); tracking.current.timeoutInteraction?.cancel?.(); tracking.current.timeoutInteraction = runAfterInteractions(() => { tracking.current.timeInteraction = Date.now() - tracking.current.startTime; }); /** * support for debug last element */ if (item?.componentName) { tracking.current.lastElement = item; } /** * for stop tracking when user interaction */ if (item?.interaction) { onScreenLoad(); onScreenInteraction(); onFirstInteraction(item?.action); } /** * timeout for handle tracking screen */ tracking.current.timeoutLoad = setTimeout(() => { const time = tracking.current.endTime - tracking.current.startTime; tracking.current.widgets = widgets.current; if (tracking.current.timeLoad === 0) { tracking.current.timeLoad = time; } onScreenLoad(); onScreenInteraction(); }, 2000); }, onLoading: (loading: boolean) => { if (loading && tracking.current.timeStartLoading === 0) { tracking.current.timeStartLoading = Date.now(); } /** * tracking for loading screen */ if (!loading) { tracking.current.timeEndLoading = Date.now(); } /** * timeout for handle tracking screen */ clearTimeout(tracking.current.timeoutLoading); tracking.current.timeoutLoading = setTimeout(() => { onScreenLoading(); }, 2000); }, onSetParams: (item: ScreenTrackingParams) => { tracking.current.params = item; }, }} > {showGrid && } ); }; export default StackScreen;