ts/features/messages/utils/messages.ts
/**
* Generic utilities for messages
*/
import * as E from "fp-ts/lib/Either";
import * as O from "fp-ts/lib/Option";
import { Predicate } from "fp-ts/lib/Predicate";
import { identity, pipe } from "fp-ts/lib/function";
import FM from "front-matter";
import { Linking } from "react-native";
import { MessageBodyMarkdown } from "../../../../definitions/backend/MessageBodyMarkdown";
import { ServiceId } from "../../../../definitions/backend/ServiceId";
import { ServiceMetadata } from "../../../../definitions/backend/ServiceMetadata";
import { Locales } from "../../../../locales/locales";
import {
deriveCustomHandledLink,
isIoFIMSLink,
isIoInternalLink,
removeFIMSPrefixFromUrl
} from "../../../components/ui/Markdown/handlers/link";
import { trackMessageCTAFrontMatterDecodingError } from "../analytics";
import { localeFallback } from "../../../i18n";
import NavigationService from "../../../navigation/NavigationService";
import { CTA, CTAS, MessageCTA, MessageCTALocales } from "../types/MessageCTA";
import {
getInternalRoute,
handleInternalLink
} from "../../../utils/internalLink";
import { getLocalePrimaryWithFallback } from "../../../utils/locale";
import { FIMS_ROUTES } from "../../fims/common/navigation";
export type CTAActionType =
| "io_handled_link"
| "io_internal_link"
| "fims"
| "none";
export const handleCtaAction = (
cta: CTA,
linkTo: (path: string) => void,
preActionCallback?: (actionType: CTAActionType) => void
) => {
if (isIoInternalLink(cta.action)) {
preActionCallback?.("io_internal_link");
const convertedLink = getInternalRoute(cta.action);
handleInternalLink(linkTo, `${convertedLink}`);
return;
} else if (isIoFIMSLink(cta.action)) {
preActionCallback?.("fims");
const url = removeFIMSPrefixFromUrl(cta.action);
NavigationService.navigate(FIMS_ROUTES.MAIN, {
screen: FIMS_ROUTES.CONSENTS,
params: {
ctaText: cta.text,
ctaUrl: url
}
});
return;
} else {
const maybeHandledAction = deriveCustomHandledLink(cta.action);
if (E.isRight(maybeHandledAction)) {
preActionCallback?.("io_handled_link");
Linking.openURL(maybeHandledAction.right.url).catch(() => 0);
return;
}
}
preActionCallback?.("none");
};
const hasMetadataTokenName = (metadata?: ServiceMetadata): boolean =>
metadata?.token_name !== undefined;
// a mapping between routes name (the key) and predicates (the value)
// the predicate says if for that specific route the navigation is allowed
const internalRoutePredicates: Map<
string,
Predicate<ServiceMetadata | undefined>
> = new Map<string, Predicate<ServiceMetadata | undefined>>([
["/services/webview", hasMetadataTokenName]
]);
/**
* since remote payload can have a subset of supported locales, this function
* return the locale supported by the app. If the remote locale is not supported
* a fallback will be returned
*/
export const getRemoteLocale = (): Extract<Locales, MessageCTALocales> =>
pipe(
getLocalePrimaryWithFallback(),
MessageCTALocales.decode,
E.getOrElseW(() => localeFallback.locale)
);
const extractCTAs = (
text: string,
serviceMetadata?: ServiceMetadata,
serviceId?: ServiceId
): O.Option<CTAS> =>
pipe(
text,
FM.test,
O.fromPredicate(identity),
O.chain(() =>
pipe(
E.tryCatch(() => FM<MessageCTA>(text).attributes, E.toError),
E.mapLeft(() => trackMessageCTAFrontMatterDecodingError(serviceId)),
O.fromEither
)
),
O.chain(attributes =>
pipe(
attributes[getRemoteLocale()],
CTAS.decode,
O.fromEither,
// check if the decoded actions are valid
O.filter(ctas => hasCtaValidActions(ctas, serviceMetadata))
)
)
);
/**
* Extract the CTAs if they are nested inside the message markdown content.
* The returned CTAs are already localized.
* @param markdown
* @param serviceMetadata
* @param serviceId
*/
export const getMessageCTA = (
markdown: MessageBodyMarkdown | string,
serviceMetadata?: ServiceMetadata,
serviceId?: ServiceId
): O.Option<CTAS> => extractCTAs(markdown, serviceMetadata, serviceId);
/**
* extract the CTAs from a string given in serviceMetadata such as the front-matter of the message
* if some CTAs are been found, the localized version will be returned
* @param serviceMetadata
*/
export const getServiceCTA = (
serviceMetadata?: ServiceMetadata
): O.Option<CTAS> =>
pipe(
serviceMetadata?.cta,
O.fromNullable,
O.chain(cta => extractCTAs(cta, serviceMetadata))
);
/**
* return a boolean indicating if the cta action is valid or not
* Checks on servicesMetadata for defined parameter based on predicates defined in internalRoutePredicates map
* @param cta
* @param serviceMetadata
*/
const isCtaActionValid = (
cta: CTA,
serviceMetadata?: ServiceMetadata
): boolean => {
// check if it is an internal navigation
if (isIoInternalLink(cta.action)) {
const internalRoute = getInternalRoute(cta.action);
return pipe(
internalRoutePredicates.get(internalRoute),
O.fromNullable,
O.map(f => f(serviceMetadata)),
O.getOrElse(() => true)
);
}
if (isIoFIMSLink(cta.action)) {
return pipe(
E.tryCatch(
() => new URL(cta.action),
() => false
),
E.fold(identity, _ => true)
);
}
// check if it is a custom action (it should be composed in a specific format)
const maybeCustomHandledAction = deriveCustomHandledLink(cta.action);
return E.isRight(maybeCustomHandledAction);
};
/**
* return true if at least one of the CTAs is valid
* @param ctas
* @param serviceMetadata
*/
const hasCtaValidActions = (
ctas: CTAS,
serviceMetadata?: ServiceMetadata
): boolean => {
const isCTA1Valid = isCtaActionValid(ctas.cta_1, serviceMetadata);
if (ctas.cta_2 === undefined) {
return isCTA1Valid;
}
const isCTA2Valid = isCtaActionValid(ctas.cta_2, serviceMetadata);
return isCTA1Valid || isCTA2Valid;
};
/**
* remove the cta front-matter if it is nested inside the markdown
* @param markdown
*/
export const cleanMarkdownFromCTAs = (
markdown: MessageBodyMarkdown | string
): string =>
pipe(
markdown,
FM.test,
O.fromPredicate(identity),
O.map(() => FM(markdown).body),
O.getOrElse(() => markdown as string)
);