import { FC, ReactNode, useEffect, useRef, useState } from 'react'; import { ActivityIndicator, Image, Modal, ModalProps, Platform, Pressable, SafeAreaView, StyleSheet, View, } from 'react-native'; import { WebView } from 'react-native-webview'; import { useConstantCallback } from '../hooks/useConstant'; import { ErrorDisplay } from './ErrorDisplay'; import { KomoEvent } from '../models/KomoEvent'; import { useEmbedMessageHandler } from '../hooks/useEmbedMessageHandler'; import { KomoWebView, KomoWebViewProps } from './KomoWebView'; export const ReactNativeWebViewName = 'ReactNativeWebView'; export const ShareClickUrlParamName = 'shareClickUrl'; type OwnProps = { /** * Whether the modal is open. */ isOpen: boolean; /** * Callback for when close is requested. */ onClose: () => void; /** * Override the default modal header. */ modalHeader?: ReactNode; /** * Override the url that redirects a user when clicking on a share link */ shareClickUrl?: string; /** * An identifier for the embedded Komo content to identify where it's being embedded */ appId?: string; /** * Override the default loading indicator. */ loadingIndicator?: ReactNode; /** * Override the default modal props. */ modalProps?: Omit; /** * Timeout in milliseconds before showing error state if loading takes too long * Defaults to 15000ms (15 seconds) */ loadingTimeoutMs?: number; /** * Override the default error display */ errorDisplay?: ({ onRetry }: { onRetry: () => void }) => ReactNode; /** * Callback for when a komo-event is raised in the embedded experience. * @param eventData - The KomoEvent data. */ onKomoEvent?: (eventData: KomoEvent) => void; /** * Callback for when a window message is raised in the embedded experience. * Note that komo-events are also window messages, so this callback will be called for komo-events as well. * @param eventData - The event data. */ onWindowMessage?: (eventData: any) => void; }; export type ExperienceModalProps = OwnProps & Pick< KomoWebViewProps, | 'embedUrl' | 'embedAuthUrl' | 'authPassthroughParams' | 'formPrefillValues' | 'extensionDataValues' | 'iframeProps' | 'webViewProps' | 'onFileDownload' >; export const ExperienceModal: FC = ({ isOpen, onClose, embedUrl, embedAuthUrl, authPassthroughParams, modalHeader: modalHeaderOverride, shareClickUrl, appId, formPrefillValues, extensionDataValues, loadingIndicator, modalProps, loadingTimeoutMs = 15000, errorDisplay, onKomoEvent, onWindowMessage, webViewProps, iframeProps, onFileDownload, }) => { const [experienceLoading, setExperienceLoading] = useState(true); const [showHeader, setShowHeader] = useState(true); const [isError, setIsError] = useState(false); const [retryCounter, setRetryCounter] = useState(0); const webviewRef = useRef(null); const iframeRef = useRef(null); const embedId = appId ? `RN::${Platform.OS}::EmbedId::${appId}` : `RN::${Platform.OS}::EmbedId::Untitled`; const handleClose = useConstantCallback(() => { setExperienceLoading(true); setIsError(false); onClose(); }); const handleRetry = useConstantCallback(() => { setExperienceLoading(true); setIsError(false); setRetryCounter((s) => s + 1); if (Platform.OS === 'web') { iframeRef.current?.contentWindow?.location.reload(); } else { webviewRef.current?.reload(); } }); const { handleMessage } = useEmbedMessageHandler({ embedId, shareClickUrl, isOpen, onClose: handleClose, onKomoEvent, onWindowMessage, setExperienceLoading, setShowHeader, iframeRef, webviewRef, }); useEffect(() => { if (!isOpen) return; // Set new timeout const timeout = setTimeout(() => { if (experienceLoading) { setIsError(true); } }, loadingTimeoutMs); return () => clearTimeout(timeout); }, [isOpen, experienceLoading, loadingTimeoutMs, retryCounter]); const isLoading = experienceLoading; return ( {isLoading && !isError && ( {loadingIndicator || } )} {!!modalHeaderOverride && ( {modalHeaderOverride} )} {showHeader && !modalHeaderOverride && ( )} {isError && (!errorDisplay ? ( ) : ( errorDisplay({ onRetry: handleRetry }) ))} {!!embedUrl && !isError && ( )} ); }; const styles = StyleSheet.create({ modalView: { flex: 1, backgroundColor: 'rgba(0,0,0,0.9)', borderRadius: 0, padding: 0, alignItems: 'center', justifyContent: 'center', elevation: 5, }, modalHeader: { backgroundColor: 'white', width: '100%', borderBottomWidth: 1, borderBottomColor: '#bfbfbf', padding: 12, }, headerOverridePressable: { width: '100%', }, closeButton: { width: 40, padding: 6, }, loadingIndicator: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', zIndex: 1000, }, errorContainer: { flex: 1, width: '100%', backgroundColor: '#f8f8f8', justifyContent: 'center', }, });