/* eslint-disable @typescript-eslint/no-explicit-any */ import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; import { Linking, Platform, View } from 'react-native'; import { useSelector } from 'react-redux'; import { WebView, WebViewMessageEvent } from 'react-native-webview'; import { useSafeAreaDimensions } from '../hooks/useSafeAreaDimensions'; import html, { HTML_PLACEHOLDER } from './html'; import { RequestRefreshEvent, UnitComponentsMessage, RequestDownloadEvent, RequestOpenLinkEvent, MultiFactorAuthenticationFinishedEvent, } from '../messages/webMessages/unitComponentsMessages'; import { getHtmlBody } from '../scripts/html/bodyHtml'; import { fetchUnitScript } from '../unitComponentsSdkManager/UnitComponentsSdk.api'; import { UnitComponentsSDK } from '../unitComponentsSdkManager/UnitComponentsSdkManager'; import type { WebViewMessage } from '../messages/webMessages'; import { getInfoParams, handleRequestDownload, injectEventToContinue } from './WebComponent.utils'; import { PresentationMode, WebComponentType } from '../types/internal/webComponent.types'; import { getFontFacesString } from '../scripts/html/fontFaces'; import type { RootState } from '../store'; import { eventBus } from '../utils/eventBus'; import AppInfo from '../utils/AppInfo'; import UNStoreManagerHelper from '../nativeModulesHelpers/UNStoreModuleHelper/UNStoreModuleHelper'; import { UserDataKeys } from '../types/internal/unitStore.types'; import { useEventListener } from '../hooks/useEventListener'; import { setItemInWindowUnitStore } from '../utils/windowUnitStore'; export interface WebComponentProps { type: WebComponentType; presentationMode?: PresentationMode, params?: string; theme?: string; language?: string; onMessage?: (message: WebViewMessage) => void; script?: string; isScrollable?: boolean, nestedScrollEnabled?: boolean, handleScroll?: (event: any) => void, windowParams?: string; } export const WebComponent = React.forwardRef(function WebComponent(props, webOutRef) { const unitScript = useSelector((state: RootState) => state.configuration.unitScript); const globalTheme = useSelector((state: RootState) => state.configuration.theme); const globalLanguage = useSelector((state: RootState) => state.configuration.language); const customerToken = useSelector((state: RootState) => state.configuration.customerToken); const [sourceHtml, setSourceHtml] = useState(null); const [baseName, setBaseName] = useState(); const [androidPackageName, setAndroidPackageName] = useState(); const [infoParams, setInfoParams] = useState<{ [key: string]: string }>({}); const webRef = useRef(null); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion useImperativeHandle(webOutRef, () => webRef.current!); useEffect(() => { const getAppName = async () => { // For iOS, we extend the app name from the HTML to display a prettier access request message. // On Android, there is no request message sent from HTML. try { if (Platform.OS == 'ios') { const name = await AppInfo.getAppName(); setBaseName(name.replace(/ /g, '-')); } else { // android setBaseName('unit'); const packageName = await AppInfo.getAppIdentifier(); setAndroidPackageName(packageName); } } catch (error: any) { console.error(error); } }; const updateInfoParams = async () => { const infoParams = await getInfoParams(); setInfoParams(infoParams); }; getAppName(); updateInfoParams(); }, []); useEffect(() => { if (!unitScript) { fetchUnitScript(); return; } const updateSourceHTML = async () => { const componentCurrentTheme = props.theme ?? globalTheme; const componentCurrentLanguage = props.language ?? globalLanguage; const themeParam = componentCurrentTheme ? ` theme="${componentCurrentTheme}"` : ''; const languageParam = componentCurrentLanguage ? ` language="${componentCurrentLanguage}"` : ''; const componentsRequiresExternalTokenInsertion = [WebComponentType.whiteLabelApp]; const customerTokenParam = (componentsRequiresExternalTokenInsertion.includes(props.type)) ? '' : `customer-token="${customerToken}"\n`; const componentParams = customerTokenParam + (props.params || '') + themeParam + languageParam; const fontFaces = getFontFacesString(UnitComponentsSDK.getFonts(), UnitComponentsSDK.iosFontBase64Map); const windowInfoParams = `window.UnitMobileSDKConfig['info'] = ${JSON.stringify(infoParams)};`; const plaidRedirectUriParam = (Platform.OS == 'ios' && UnitComponentsSDK.helpers.redirectUri) ? `window.UnitMobileSDKConfig['plaidRedirectUri'] = '${UnitComponentsSDK.helpers.redirectUri}/plaid';` : ''; const androidPackageNameParam = androidPackageName ? `window.UnitMobileSDKConfig.androidPackageName='${androidPackageName}';` : ''; const unitSessionIdParam = `window.UnitSessionStore.unitSessionId = '${UnitComponentsSDK.helpers.unitSessionId}';`; const unitVerifiedCustomerToken = await UNStoreManagerHelper.getValue(UserDataKeys.unitVerifiedCustomerToken); const unitAppFormVerifiedToken = await UNStoreManagerHelper.getValue(UserDataKeys.unitApplicationFormVerifiedToken); const windowVerifiedCustomerToken = unitVerifiedCustomerToken ? `window.UnitStore['${UserDataKeys.unitVerifiedCustomerToken}'] = '${unitVerifiedCustomerToken}';` : ''; const windowAppFormVerifiedToken = unitAppFormVerifiedToken ? `window.UnitStore['${UserDataKeys.unitApplicationFormVerifiedToken}'] = '${unitAppFormVerifiedToken}';` : ''; const windowParams = `${windowInfoParams} ${unitSessionIdParam} ${plaidRedirectUriParam} ${androidPackageNameParam} ${windowVerifiedCustomerToken} ${windowAppFormVerifiedToken} ${props.windowParams || ''}`; let newHtml = html.replace(HTML_PLACEHOLDER.BODY, getHtmlBody(props.type.valueOf(), componentParams, props.presentationMode)); newHtml = newHtml.replace(HTML_PLACEHOLDER.FONT_FACES, fontFaces); newHtml = newHtml.replace(HTML_PLACEHOLDER.SCRIPT_FROM_NATIVE, props.script || ''); newHtml = newHtml.replace(HTML_PLACEHOLDER.WINDOW_PARAMS, windowParams); setSourceHtml(newHtml); }; updateSourceHTML(); }, [props.params, unitScript, props.presentationMode, props.script, props.windowParams, globalTheme, globalLanguage, customerToken, infoParams, androidPackageName]); // Listen and update the live webComponents const handleMultiFactorAuthFinished = (data: MultiFactorAuthenticationFinishedEvent) => { setItemInWindowUnitStore(webRef.current, data.tokenStoreKey as UserDataKeys, data.tokenString); injectEventToContinue(webRef.current, { parentInstanceId: data.parentInstanceId, eventToContinue: data.eventToContinue }); }; useEventListener({ busEventKey: UnitComponentsMessage.UNIT_MULTI_FACTOR_AUTH_FINISHED, action: handleMultiFactorAuthFinished }); const onMessage = (e: WebViewMessageEvent) => { const message = JSON.parse(e.nativeEvent.data) as WebViewMessage; switch (message.type) { case UnitComponentsMessage.UNIT_REQUEST_REFRESH: message.details && eventBus.emit( UnitComponentsMessage.UNIT_REQUEST_REFRESH, message.details as RequestRefreshEvent ); break; case UnitComponentsMessage.UNIT_REQUEST_OPEN_LINK: // eslint-disable-next-line no-case-declarations const { href } = (message.details as RequestOpenLinkEvent); Linking.openURL(href); break; case UnitComponentsMessage.UNIT_REQUEST_DOWNLOAD: if (message.details) { handleRequestDownload(message.details as RequestDownloadEvent).then((result) => { const urlArg = JSON.stringify(result.url); const errorArg = result.error ? JSON.stringify(result.error) : 'undefined'; webRef.current?.injectJavaScript(`dispatchOnDownload(${urlArg}, ${errorArg})`); eventBus.emit(UnitComponentsMessage.UNIT_REQUEST_CLOSE_FLOW, {}); }); } break; case UnitComponentsMessage.UNIT_MULTI_FACTOR_AUTH_FINISHED: if (message.details) { const data = message.details as MultiFactorAuthenticationFinishedEvent; UNStoreManagerHelper.saveValue(data.tokenStoreKey as UserDataKeys, data.tokenString); // update existing components - namely, the other webComponents will update their window as well as this webComponent eventBus.emit( UnitComponentsMessage.UNIT_MULTI_FACTOR_AUTH_FINISHED, data ); } props.onMessage && props.onMessage(message); break; case UnitComponentsMessage.UNIT_UNAUTHORIZED_TOKEN: UnitComponentsSDK.cleanUserData(); break; default: props.onMessage && props.onMessage(message); } }; if (!sourceHtml) return null; const _onScroll = (event: any) => { if (props.handleScroll) { props.handleScroll(event); } }; if (!baseName) { return null; } const isInheritMode = props.presentationMode === PresentationMode.Inherit; const { effectiveBottomInset } = useSafeAreaDimensions(); const needsBottomPadding = isInheritMode && effectiveBottomInset > 0; const webView = ( ); if (needsBottomPadding) { return ( {webView} ); } return webView; });