@applicaster/zapp-react-native-utils
Version:
Applicaster Zapp React Native utilities package
221 lines (190 loc) • 5.71 kB
text/typescript
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;
}