UNPKG

@applicaster/zapp-react-native-utils

Version:

Applicaster Zapp React Native utilities package

221 lines (190 loc) • 5.71 kB
import { useMemo } from "react"; import * as R from "ramda"; import { ZappPipesEntryContext, ZappPipesScreenContext, ZappPipesSearchContext, } from "@applicaster/zapp-react-native-ui-components/Contexts"; import { inflateString } from "../../stringUtils"; import { useRoute } from "../navigation"; import { getMissingValues } from "../../inflatedUrlUtils"; import { reactHooksLogger as logger } from "../logger"; import { isTV } from "../../reactUtils"; import { findEndpointForURL, HTTP_METHODS, } from "@applicaster/zapp-pipes-v2-client"; import { appStore } from "@applicaster/zapp-react-native-redux/AppStore"; import { ENDPOINT_TAGS } from "../../types"; /** * will match any occurrence in a string of one or more word characters * surrounded by double curly braces. * * For example, it would match {{username}} or {{email}} in a string * */ const INTERPOLATED_WORD_REGEX = /{{\w+}}/g; const handleBarKey = (string) => `{{${string}}}`; /** * returns a url inflated with the pipes v2 params if needed * @params {Object} options * @params {string} options.source the url from the component configuration * @params {Object} options.contexts the contexts objects to provide (search, entry, screen) * @params {Object} options.mappping the mapping dictionary to get the property path, * the source (context), and the key name * @returns { string} */ interface GetInflatedDataSourceUrl { (properties: { source?: string; contexts: { entry?: ZappEntry; screen?: ZappRiver; search?: object | string; }; mapping?: ZappTypeMapping; }): string | null; } export const getInflatedDataSourceUrl: GetInflatedDataSourceUrl = ({ source, contexts, mapping, }) => { /** * example of source: https://foo.com/shows/{showId} * example of mapping: * { * showId: { source: "entry", property: "extensions.showId" } * } * example of context: * { * entry: { extensions: { showId: A1234 }} * screen: { ... } * search: { query: "..." } * } * * defines string to inflate as `https://foo.com/shows/{{entry.extensions.showId}}` * and the correct property will be retrieved from the context object to return * https://foo.com/shows/A1234 */ if (!source) { // eslint-disable-next-line no-console console.error("source is empty", { source, contexts, mapping, }); return null; } // Hack because in tv we expect to get key names instead of values from the fake entry const newSource = contexts.entry?.isDeepLink && isTV() ? source.replace( INTERPOLATED_WORD_REGEX, (key) => contexts?.entry?.[key.replace(/{{|}}/g, "")] ?? key ) : source; const string = R.reduce( (string, mappingKey) => R.replace( handleBarKey(mappingKey), handleBarKey( R.join(".", [ R.path([mappingKey, "source"], mapping), R.path([mappingKey, "property"], mapping), ]) ), string ), newSource, R.keys(mapping) ); const missingValues = getMissingValues(contexts, string); if (missingValues && missingValues.length > 0) { const pipesEndpoints = appStore.get("pipesEndpoints"); const endpointURL = findEndpointForURL( source, pipesEndpoints, HTTP_METHODS.GET ); const tags = pipesEndpoints?.[endpointURL]?.tags; const allowMissingKeys = tags?.includes(ENDPOINT_TAGS.allow_missing_keys); if (!allowMissingKeys) { const message = `Not enough data to fully inflate url: ${missingValues.join( "," )} source: ${source}`; // todo: add js only payload instead if (__DEV__) { logger.warning({ message, data: { source, contexts, mapping }, }); } else { // todo: log values we've failed to resolve logger.warning({ message, }); } // return null in order to prevent start fetching data with invalid url return null; } else { logger.debug({ message: `Ignoring missing values in url because ${ ENDPOINT_TAGS.allow_missing_keys } endpoint tag, keys: ${missingValues.join(",")}`, }); } } return inflateString({ string, data: contexts }); }; const encodeIfNeeded: (string) => string = R.tryCatch( R.when((s) => decodeURIComponent(s) === s, encodeURIComponent), R.flip(encodeURIComponent) ); export function getSearchContext( searchContext: string, mapping: ZappTypeMapping ) { if (!mapping) { return {}; } const { property }: { property: string } = R.compose( R.find(R.propEq("source", "search")), R.values )(mapping) || { property: "q" }; return { [property]: encodeIfNeeded(searchContext) }; } export function useInflatedUrl({ feedUrl, mapping, }: { feedUrl?: string; mapping?: ZappTypeMapping; }) { const { pathname } = useRoute(); const [entryContext] = ZappPipesEntryContext.useZappPipesContext(pathname); const [searchContext] = ZappPipesSearchContext.useZappPipesContext(); const [screenContext] = ZappPipesScreenContext.useZappPipesContext(); const url = useMemo( () => mapping ? getInflatedDataSourceUrl({ source: feedUrl, contexts: { entry: entryContext, screen: screenContext, search: getSearchContext(searchContext, mapping), }, mapping, }) : feedUrl, [feedUrl, mapping] ); if (!feedUrl) { logger.warning({ message: "Required parameter feedUrl is missing", data: { feedUrl }, }); return null; } return url; }