@applicaster/zapp-react-native-utils
Version:
Applicaster Zapp React Native utilities package
492 lines (386 loc) • 13.8 kB
text/typescript
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint max-len: off */
import React, { useContext, useEffect, useMemo } from "react";
import { BackHandler, InteractionManager, NativeModules } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import {
useContentTypes,
usePickFromState,
} from "@applicaster/zapp-react-native-redux/hooks";
import { HooksManager } from "@applicaster/zapp-react-native-utils/appUtils/HooksManager";
import { legacyScreenData } from "@applicaster/quick-brick-core/App/NavigationProvider/utils";
import {
LONG_KEY_PRESS_TIMEOUT,
QUICK_BRICK_NAVBAR,
QUICK_BRICK_CONTENT,
} from "@applicaster/quick-brick-core/const";
import { useZappHooksModalState } from "@applicaster/zapp-react-dom-ui-components/Components/ZappHooksModal/hooks/useZappHooksModalState";
import { ZappHookModalContext } from "@applicaster/zapp-react-native-ui-components/Contexts";
import { PathnameContext } from "@applicaster/zapp-react-native-ui-components/Contexts/PathnameContext";
import { HookModalContextT } from "@applicaster/zapp-react-native-ui-components/Contexts/ZappHookModalContext";
import { HOOKS_EVENTS } from "../../appUtils/HooksManager/constants";
import {
getNavigationProps,
getRiverFromRoute,
getTargetRoute,
} from "../../navigationUtils";
import { platformSelect } from "../../reactUtils";
import { useConnectionInfo } from "../connection";
import { getTargetScreenData } from "../screen/useTargetScreenData";
import { isTV, isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
import { useNavbarState } from "../screen";
import { useNavigation } from "./useNavigation";
import { useModalStore } from "../../modalState";
import { ScreenDataContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenDataContext";
export { useNavigation } from "./useNavigation";
/**
* This function helps to decide wether the navbar should be presented on the screen
* based on route and / or screen Data
*
* Currently the scenarios include
* - players => false
* - search screen plugin => false
* - hooks with `showNavBar: true` or `presentFullScreen` not set to true, defaults to no navbar for hook screens
* - screens with `allow_screen_plugin_presentation` in general property => false
*
* @param {String} route current route of the screen
* @param {Object} screenData payload associated with the currently presented screen
* @returns {Boolean}
*/
export function isNavBarVisible(
route: string,
screenData: ZappRiver = {} as ZappRiver,
showNavBar: boolean,
canGoBack: boolean,
videoModalState: QuickBrickVideoModalState | null = null
) {
/* screenData is not actual navigator.data
* or navigator.screenData,
* but navigator.data.screen */
if (route.includes("playable")) {
return false;
}
if (
videoModalState?.mode === "FULLSCREEN" &&
videoModalState?.visible === true
) {
return false;
}
if (screenData?.type === "qb_search_screen" && !isTV()) {
return false;
}
if (screenData?.plugin_type === "login" && isTV()) {
return false;
}
/* Match if screen is Hook */
if (route.startsWith("/hooks/")) {
const module = screenData?.module;
if (module?.presentFullScreen) {
return false;
}
return module?.showNavBar ?? false;
}
if (screenData?.hookPlugin) {
const hookPlugin = screenData?.hookPlugin?.module;
return (
hookPlugin?.showNavBar || hookPlugin?.presentFullScreen !== true || false
);
}
if (
screenData?.general?.allow_screen_plugin_presentation ||
screenData?.general?.hide_app_nav_bar
) {
return false;
}
const hideSecondaryLevel = screenData?.navigations?.some(
(navigation) => navigation.styles?.hide_secondary_level_menu
);
if (canGoBack && hideSecondaryLevel) {
return false;
}
if (typeof showNavBar === "boolean" && !showNavBar) {
return false;
}
return true;
}
const { ProfilerBridge } = NativeModules;
export const usePathname = () => React.useContext(PathnameContext);
export const useBackHandler = (cb: () => boolean) => {
React.useEffect(() => {
BackHandler.addEventListener("hardwareBackPress", cb);
return () => {
BackHandler.removeEventListener("hardwareBackPress", cb);
};
}, [cb]);
};
// starts with modal/
const isModalPathname = (pathname: string) => /^modal\//.test(pathname);
const isVideoModalPathname = (pathname: string) =>
/^video-modal\//.test(pathname);
const isHookModalPathname = (pathname: string) =>
/^hooks-modal\//.test(pathname);
const isHookPathname = (pathname: string) => /^\/hooks\//.test(pathname);
type VariousScreenData = LegacyNavigationScreenData | ZappRiver | ZappEntry;
export const useRoute = (): {
screenData: VariousScreenData;
pathname: string;
} => {
const pathname = usePathname() || "";
const navigator = useNavigation();
const screenDataContext = legacyScreenData(useContext(ScreenDataContext));
const { plugins, contentTypes, rivers } = usePickFromState([
"plugins",
"rivers",
"contentTypes",
]);
const { modalState } = useModalStore();
const hookModalState = useZappHooksModalState();
const modalScreenData = modalState.screen;
const hookModalScreenData = hookModalState.state.screenData?.payload;
const videoModalScreenData =
navigator.videoModalState?.item &&
legacyScreenData(
{
entry: navigator.videoModalState?.item as ZappEntry,
screen: getTargetScreenData(
navigator.videoModalState?.item as ZappEntry,
rivers,
contentTypes
),
},
plugins
);
// There are 4 route scenarios
// For regular screens take date from navigator stack.
// is path is model grab screenData from modal state
// if path is video modal grab screenData from video state
// if path is hook modal grab screenData from hook state
// if path is hook grab screenData from screenData
if (isModalPathname(pathname)) {
const screenData = modalScreenData ?? ({} as ZappEntry);
return { screenData, pathname };
}
if (isVideoModalPathname(pathname)) {
const screenData = videoModalScreenData as LegacyNavigationScreenData;
return { screenData, pathname };
}
if (isHookModalPathname(pathname)) {
const screenData = hookModalScreenData;
return { screenData, pathname };
}
if (isHookPathname(pathname)) {
return { screenData: screenDataContext, pathname };
}
const screenData = screenDataContext;
return { screenData, pathname };
};
type Callbacks = Partial<{
handleHookPresent: ({
route,
payload,
}: {
route: string;
payload: HookPluginProps;
}) => void;
handleHookError: (props: HookPluginProps & { error?: string }) => any;
handleHookCancel: (props: Omit<HookPluginProps, "callback">) => any;
handleHookComplete: (
props: Omit<HookPluginProps, "callback"> & { route: string }
) => any;
}>;
export const useZappHooksForEntry = (
entry: ZappEntry,
callbacks?: Callbacks
) => {
const {
setState,
resetState,
setIsHooksExecutionInProgress,
setIsPresentingFullScreen,
setIsRunningInBackground,
}: HookModalContextT = React.useContext(ZappHookModalContext.ReactContext);
const {
appData: { layoutVersion },
rivers,
plugins,
} = usePickFromState(["appData", "rivers", "plugins"]);
const contentTypes = useContentTypes();
const isOnline = useConnectionInfo(true);
useEffect(() => {
resetState();
if (entry) {
setIsHooksExecutionInProgress(true);
if (!isOnline) {
setIsHooksExecutionInProgress(false);
// TODO: ideally move this logic to hooks manager
// @ts-ignore
return callbacks?.handleHookComplete?.({ payload: entry });
}
const targetRoute = getTargetRoute(entry, "", {
layoutVersion,
contentTypes,
});
const targetScreen = getRiverFromRoute({ route: targetRoute, rivers });
const hooksOptions = {
rivers,
plugins,
targetScreen,
};
const handleHookPresent = ({ route, payload }) => {
setIsPresentingFullScreen();
setState({
path: route,
screenData: payload,
});
callbacks?.handleHookPresent?.({ route, payload });
};
const handleBackgroundHook = (hookProps) => {
const { route, payload } = hookProps;
setState({
path: route,
screenData: payload,
});
setIsRunningInBackground();
};
const handleHookError: Callbacks["handleHookError"] = (props) => {
setIsHooksExecutionInProgress(false);
callbacks?.handleHookError?.(props);
};
const handleHookCancel: Callbacks["handleHookCancel"] = (props) => {
setIsHooksExecutionInProgress(false);
callbacks?.handleHookCancel?.(props);
};
const handleHookComplete: Callbacks["handleHookComplete"] = (props) => {
setIsHooksExecutionInProgress(false);
callbacks?.handleHookComplete?.(props);
};
const hookManager = HooksManager(hooksOptions)
.on(HOOKS_EVENTS.PRESENT_SCREEN_HOOK, handleHookPresent)
.on(HOOKS_EVENTS.ERROR, handleHookError)
.on(HOOKS_EVENTS.CANCEL, handleHookCancel)
.on(HOOKS_EVENTS.START_BACKGROUND_HOOK, handleBackgroundHook)
.on(HOOKS_EVENTS.COMPLETE, handleHookComplete)
.handleHooks(entry);
return () => {
hookManager
.removeHandler(HOOKS_EVENTS.PRESENT_SCREEN_HOOK, handleHookPresent)
.removeHandler(HOOKS_EVENTS.ERROR, handleHookError)
.removeHandler(HOOKS_EVENTS.CANCEL, handleHookCancel)
.removeHandler(HOOKS_EVENTS.COMPLETE, handleHookComplete);
};
}
}, [entry.id]);
};
export enum MenuTypes {
drawer = "DRAWER",
bottomTabBar = "BOTTOM_TAB_BAR",
}
export const useNavigationPluginData = (): ZappNavigation | undefined => {
const {
screenData: useRouteScreenData,
}: { screenData: QuickBrickNavigationData | null } = useRoute();
const navigations = useRouteScreenData?.targetScreen
? (useRouteScreenData.targetScreen as ZappRiver).navigations
: (useRouteScreenData as ZappRiver).navigations;
const navigationMenu = navigations?.find((nav) => nav.category === "menu");
return navigationMenu;
};
export const useNavigationType = (): MenuTypes => {
const navigationMenu = useNavigationPluginData();
return !navigationMenu ||
navigationMenu.navigation_type === "quick_brick_side_menu"
? MenuTypes.drawer
: MenuTypes.bottomTabBar;
};
export const useProfilerLogging = () => {
const logTimestamp = (data: string) =>
InteractionManager.runAfterInteractions(() => {
if (ProfilerBridge && data) {
ProfilerBridge.updateTimer("navigation", data, Date.now());
}
});
return logTimestamp;
};
export const useRunIfLongPress = (
options = { timeout: LONG_KEY_PRESS_TIMEOUT }
) => {
const pressTimeout =
React.useRef<Nullable<ReturnType<typeof setTimeout>>>(null);
const resetTimeout = () => {
clearTimeout(pressTimeout.current as ReturnType<typeof setTimeout>);
};
const startTimeout = (cb: () => void) => {
pressTimeout.current = setTimeout(cb, options.timeout);
};
const onPress = (cb: () => void) => {
startTimeout(cb);
};
const onPressOut = () => {
resetTimeout();
};
return { onPress, onPressOut };
};
/**
* @returns boolean - navbar visiblity status
*/
export const useIsNavBarVisible = (): boolean => {
const { visible } = useNavbarState();
return useMemo(() => visible, [visible]);
};
const useGetNavBarTopBorderWidth = (): number => {
const navigator = useNavigation();
const navProps = getNavigationProps({
navigator,
title: "",
category: "menu",
});
return Number(((navProps?.styles as any)?.top_border_width as number) ?? 0);
};
const TAB_BAR_HEIGHT_IOS = 49;
const TAB_BAR_HEIGHT_ANDROID = 56;
const useGetTabBarHeight = () =>
platformSelect({
ios: TAB_BAR_HEIGHT_IOS + (useSafeAreaInsets().bottom ?? 0),
android: TAB_BAR_HEIGHT_ANDROID,
}) as number;
export const useGetBottomTabBarHeight = (): number => {
const topBorderWidth = useGetNavBarTopBorderWidth();
const tabBarHeight = useGetTabBarHeight();
const isBottomBarNavigation = useNavigationType() === MenuTypes.bottomTabBar;
return !isBottomBarNavigation ? 0 : tabBarHeight + topBorderWidth;
};
// If current screen is active/focused (visible to the user)
export const useIsScreenActive = () => {
const pathname = usePathname();
const { currentRoute, videoModalState } = useNavigation();
if (
videoModalState.visible &&
["FULLSCREEN", "MAXIMIZED", "PIP"].includes(videoModalState.mode)
) {
return false;
}
return pathname === currentRoute;
};
type Return = (prefix: string) => string;
export const useUniqueRouteSuffix = (): Return => {
const pathname = usePathname();
const { currentRoute } = useNavigation();
const suffix = `currentRoute:${currentRoute}_pathname:${pathname}`;
const withUniqueRouteSuffix = React.useCallback((prefix: string) => {
if (isWeb()) {
// we should use extended route prefix only on Web, where we have stack-navigator
return `${prefix}___${suffix}`;
}
// for other platforms use previous version
return prefix;
}, []);
return withUniqueRouteSuffix;
};
export const useNavbarId = () => {
const withUniqueRouteSuffix = useUniqueRouteSuffix();
return withUniqueRouteSuffix(QUICK_BRICK_NAVBAR);
};
export const useContentId = () => {
const withUniqueRouteSuffix = useUniqueRouteSuffix();
return withUniqueRouteSuffix(QUICK_BRICK_CONTENT);
};