UNPKG

@applicaster/zapp-react-native-utils

Version:

Applicaster Zapp React Native utilities package

436 lines (383 loc) • 13.2 kB
/// <reference types="@applicaster/applicaster-types" /> import * as R from "ramda"; import { Platform } from "react-native"; import { applyTransform } from "../transform"; import { ManifestField } from "../types"; import { isNilOrEmpty } from "../reactUtils/helpers"; export type PluginConfiguration = Record<string, string>; type TBooleanLike = boolean | string | number; type TAnyValue = unknown; type GetValue = { (key: string): string; (key: string): number; (key: string): null; }; declare type StringTransformations = | "default" | "uppercase" | "lowercase" | "toUpper" | "capitalise"; declare type Configuration = { target_screen_switch: boolean; target: string; display_mode: string; display_mode_tab_width: number; display_mode_tabs_alignment: string; text_label_data_key: string; text_label_custom_data_key: string; text_label_ios_font_family: string; text_label_default_font_color: string; text_label_active_font_color: string; text_label_selected_default_font_color: string; text_label_selected_active_font_color: string; text_label_android_font_family: string; text_label_font_size: number; text_label_line_height: number; text_label_ios_letter_spacing: number; text_label_android_letter_spacing: number; text_label_text_transform: StringTransformations; tab_bar_background_color: string; tab_bar_elevation: number; tab_bar_shadow_color: string; tab_bar_shadow_offset_width: number; tab_bar_shadow_offset_height: number; tab_bar_shadow_radius: number; tab_bar_padding_top: number; tab_bar_padding_right: number; tab_bar_padding_bottom: number; tab_bar_padding_left: number; tab_bar_gutter: number; tab_bar_border_bottom_width: number; tab_bar_border_bottom_color: string; tab_cell_background_color_default: string; tab_cell_background_color_active: string; tab_cell_background_color_selected_default: string; tab_cell_background_color_selected_active: string; tab_cell_padding_top: number; tab_cell_padding_right: number; tab_cell_padding_bottom: number; tab_cell_padding_left: number; tab_cell_border_radius: number; tab_cell_border_width: number; tab_cell_border_color_default: string; tab_cell_border_color_default_focused: string; tab_cell_border_color_active: string; tab_cell_border_color_active_focused: string; tab_cell_indicator_height: number; tab_cell_indicator_color: string; tab_cell_indicator_border_radius: number; sticky_tab_bar: boolean; components_container_background_color?: string; components_container_padding_top?: number; components_container_padding_bottom?: number; components_container_padding_left?: number; components_container_padding_right?: number; tablet_theme: boolean; tablet_components_container_padding_top?: number; tablet_components_container_padding_bottom?: number; tablet_components_container_padding_left?: number; tablet_components_container_padding_right?: number; }; export const getBoolFromConfigValue = (value: TBooleanLike): boolean => { if (typeof value === "boolean") { return value; } return value === "true" || value === "1" || value === 1; }; export const requiresAuthentication = (entry: ZappEntry): Nullable<boolean> => { const requires_authentication: TBooleanLike | undefined = R.path( ["extensions", "requires_authentication"], entry ); return !R.isNil(requires_authentication) ? getBoolFromConfigValue(requires_authentication) : null; }; /** * Flattens the manifest configuration fields which contains group into * a flat list of fields * @param fields to flatten * @returns flatten fields */ export function flattenFields( fields: ManifestField<TAnyValue>[] = [] ): ManifestField<TAnyValue>[] { return fields.reduce((acc, field) => { if (field.fields) { return [...acc, ...flattenFields(field.fields)]; } else { return [...acc, field]; } }, []); } /** * Retrieves the value of the "src" in the first media_item * that has the matching key provided in args. * Fallbacks: "image_base" key, or first media_item that has any "src" * @param {Object} entry Single entry from a feed * @param {Array} arg Array with a single element - the key of the media item * from which the "src" should be retrieved * @returns {?String} Value of "src", usually a URI */ export function imageSrcFromMediaItem( entry: ZappEntry, arg: string[] | unknown ) { const args: unknown = R.unless(Array.isArray, Array)(arg || []); const imageKey: string = args?.[0] || "image_base"; // always a single key in this function const mediaGroup = R.path<ZappMediaGroup[]>(["media_group"], entry); if (!mediaGroup) { return undefined; } const hasTypeImageOrThumbnail = R.either( R.propEq("type", "image"), R.propEq("type", "thumbnail") ); const pickMediaItemProp = R.prop<ZappMediaItem>("media_item"); const mediaItems = R.compose( R.flatten, R.map(pickMediaItemProp), R.filter(hasTypeImageOrThumbnail) )(mediaGroup); if (!mediaItems) { return undefined; } const src = R.compose( R.prop("src"), R.defaultTo(R.head(mediaItems)), R.when(R.isNil, () => R.find(R.propEq("key", "image_base"), mediaItems)), R.find(R.propEq("key", imageKey)) )(mediaItems); // Special case for react native - uri cannot be an empty string (yellow warning). // R.isEmpty is tailored specifically for checks like these, // it returns false for undefined values. return R.isEmpty(src) ? undefined : src; } /** * map of type checks to apply to each manifest field in order to check if * the provided value is valid for that given manifest type */ const typeChecks = { switch: R.is(Boolean), checkbox: R.is(Boolean), number_input: R.is(Number), text_input: R.is(String), }; /** * This function checks if a provided value is valid for the provided type * curried function of the form isInvalidForType(type)(value); * returns true if the value is invalid, and false if the value is ok. * @param {*} type * @returns {Function} * @param {Any} value to check * @returns {boolean} */ function isInvalidForType(type: string): (arg0: any) => boolean { const typeCheck = typeChecks[type] || (() => true); return function (value: TAnyValue): boolean { return !typeCheck(value); }; } /** * Checks if a value is valid for a provided type. Checks if the the type is correct, * but also if the value is not empty, not null, and not NaN * curried function of the form isInvalidValue(type)(value). Returns true if the value * is invalid, and false otherwise * @param {String} type * @returns {Function} * @param {Any} value to check for * @returns {boolean} */ const isInvalid = (type: string, value: TAnyValue): boolean => { const validators = [R.isEmpty, R.isNil, Number.isNaN, isInvalidForType(type)]; return validators.some((valid) => valid(value)); }; function isInvalidValue(type: string): (arg0: TAnyValue) => boolean { return (value) => isInvalid(type, value); } /** * This function will sanitize a value from a provided configuration, by trying * to apply the relevant transform, and resolving to the default value * if the resulting value is invalid (null, empty, NaN, or incorrect type) * Curried function of the form castOrDefault(field)(configuration) * @param {Object} field plugin manifest field * @param {String} field.type type of the manifest field * @param {Any} field.initial_value default value provided in the manifest for that field * @param {String} field.key key of the manifest field * @returns {Function} * @param {Object} configuration retrieved from the server */ function castOrDefault<T>({ type, initial_value, key, }: ManifestField<T>): ( configuration: PluginConfiguration, skipDefaults: boolean ) => T { const transform = applyTransform(type); return function ( configuration: PluginConfiguration, skipDefaults: boolean ): T { const value = transform(configuration?.[key]); if (isInvalidValue(type)(value) && !skipDefaults) { return transform(initial_value); } return value; }; } /** * This function takes a section of configuration and checks if the relevant section * in the manifest has remapped keys. if it does, it updates to the new expected configuration * This helps not breaking the layout when a plugin configuration changes its keys * @param {Object} config key value pair of configuration for a section in a plugin * @param {Object} remappedKeys dictionary of keys to be remapped * @returns */ const applyRemappedKeys = ( config: PluginConfiguration, remappedKeys: PluginConfiguration ) => { const oldKeys = R.keys(remappedKeys || {}); const configKeys = R.keys(config); if ( !R.compose(R.length, R.intersection(oldKeys))(configKeys) || !R.length(oldKeys) ) { return config; } // need to update keys return R.reduce( (updatedConfig, oldKey: string) => { if (R.has(oldKey, config) && !R.has(remappedKeys[oldKey], config)) { return R.assoc(remappedKeys[oldKey], config[oldKey], updatedConfig); } return updatedConfig; }, config, oldKeys ); }; /** * This function updates locally the configuration for a cell style with * the updated keys if needed * @param {Object} manifest plugin manifest * @param {Object} config plugin configuration for the given cell style * @returns {Object} remapped config */ type TRemapUpdatedKeys = ( manifest: ManifestField<TAnyValue>, config: PluginConfiguration ) => PluginConfiguration; export const remapUpdatedKeys = R.curry<TRemapUpdatedKeys>( (manifest, config) => { return R.reduce( (updatedConfig, configSection: string) => { const remappedKeys = R.pathOr<string[] | null>( null, [configSection, "updated_keys"], manifest ); updatedConfig[configSection] = remappedKeys ? applyRemappedKeys(config[configSection], remappedKeys) : config[configSection]; return updatedConfig; }, {}, R.keys(config) ); } ); /** * Flattens the manifest configuration fields which contains group into * a flat list of fields and applies defaults/casts * @param fields * @param configuration * @param skipDefaults */ export function flattenAndPopulateFields( fields: ManifestField<TAnyValue>[], configuration: PluginConfiguration, skipDefaults?: boolean ): { [key: string]: any } { return fields.reduce((acc, field) => { if (field.fields) { Object.assign( acc, flattenAndPopulateFields(field.fields, configuration, skipDefaults) ); } else { acc[field.key] = castOrDefault(field)(configuration, skipDefaults); } return acc; }, {}); } /** * This function is casting default styles to expected format * @param {object} fields - array of configuration fields form manifest * @returns array of {key, value, mapper:(function)} */ type TPopulateConfigurationValues = ( fields: ManifestField<TAnyValue>[], configuration: PluginConfiguration, skipDefaults?: boolean ) => PluginConfiguration; export const populateConfigurationValues = R.curry<TPopulateConfigurationValues>( (fields, configuration, skipDefaults = false) => flattenAndPopulateFields(fields, configuration, skipDefaults) ); export const getAccesabilityProps = (item: ZappEntry) => ({ accessible: item?.extensions?.accessibility, accessibilityLabel: item?.extensions?.accessibility?.label || item?.title, accessibilityHint: item?.extensions?.accessibility?.hint, }); export const getPlayerControlsAccessibilityProps = ( icon: string, value: GetValue ) => { return { accessible: true, accessibilityLabel: value(`accessibility_${icon}_label`), accessibilityHint: value(`accessibility_${icon}_hint`), }; }; export const getAllAccessibilityProps = R.pickBy((_, key) => key.includes("accessibility") ); const os = R.compose(R.toLower, R.prop("OS"))(Platform); export const castOrDefaultValue = (mapper, defaultValue) => R.ifElse(isNilOrEmpty, R.always(defaultValue), mapper); export const castIfDefined = (mapper) => R.unless(R.isNil, mapper); export const getStyleForPlatform = R.curry( ( configuration: Partial<Configuration>, [label, key]: [string, string] ): any => R.prop(`${label}_${os}_${key}`)(configuration) ); export const capitalise = R.replace(/^./, R.toUpper); export const applyStringTransformation = ( text: string, transformation: StringTransformations ) => { const transformationMethod = R.cond([ [R.equals("default"), R.always(R.identity)], [R.equals("uppercase"), R.always(R.toUpper)], [R.equals("lowercase"), R.always(R.toLower)], [R.equals("capitalise"), R.always(capitalise)], ])(transformation); return transformationMethod(text); }; const allBetweenParenthesesRegex = /(\()(.*?)(?=\))/g; export const getOpacityFromHexColor = R.compose( parseFloat, R.trim, R.last, R.split(","), // Positive Lookbehinds are not supported in Android JS env therefor we need to remove "(" manually. R.replace("(", ""), R.head, R.match(allBetweenParenthesesRegex) );