import * as React from 'react'; import * as PropTypes from 'prop-types'; import { ConnectHost } from './ConnectHost'; import { ConnectIframeInterface } from './ConnectIframeInterface'; import { AppOptions } from '../../definitions/AppContext'; import { ConnectIframeProvider, ConnectIframeContext } from './ConnectIframeProvider'; import { logger } from '../../adaptors/logger/LoggerAdaptor'; import { analytics } from '../../adaptors/analytics/AnalyticsAdaptor'; import IFrameLifecycleEventManager from './IFrameLifecycleEventManager'; import { allowedStrings, AnalyticsCategories, AnalyticsActions } from '../../adaptors/analytics/AnalyticsConstants'; export const LoadingState = Object.freeze({ INIT: Symbol('init'), LOADED: Symbol('loaded'), FAILED: Symbol('failed'), TIMEOUT: Symbol('timeout'), LOADING: Symbol('loading'), RESOLVING: Symbol('resolving') }); module ConnectIframeDefinitions { export interface Props { connectHost: ConnectHost; appKey: string; moduleKey: string; iframeContainer: new () => React.PureComponent<{ width: string | number | undefined; height: string | number | undefined }, {}>; loadingIndicator: typeof React.PureComponent; failedToLoadIndicator: typeof React.PureComponent; timeoutIndicator: new() => React.PureComponent<{failedCallback: Function}, {}>; url: string; width: string; height: string; options: AppOptions; connectIframeProvider: ConnectIframeProvider; } export interface State { // State attributes are marked as optional so we can call setState with a subset of attributes. width?: string; height?: string; hostFrameOffset?: number; loadingState?: symbol; } } /** * ConnectIframe represents an add-on within a view. */ class ConnectIframe extends React.PureComponent { unmountCallbacks: Function[] = []; iframeContext: ConnectIframeContext; iframeAttributes: ConnectIframeInterface; iframeLifecycleEventManager: IFrameLifecycleEventManager; static propTypes: object = { connectHost: PropTypes.object.isRequired, appKey: PropTypes.string.isRequired, moduleKey: PropTypes.string.isRequired, iframeContainer: PropTypes.func, loadingIndicator: PropTypes.func, failedToLoadIndicator: PropTypes.func, timeoutIndicator: PropTypes.func, url: PropTypes.string, width: PropTypes.string, height: PropTypes.string, options: PropTypes.object, connectIframeProvider: PropTypes.object }; static defaultProps: object = { options: {} as AppOptions, connectIframeProvider: {} as ConnectIframeProvider, iframeContainer: ({ width, height, children }: { width: string | number | undefined; height: string | number | undefined; children: React.ReactNode; }) => (
{children}
), failedToLoadIndicator: () => null, loadingIndicator: () => null, timeoutIndicator: () => null }; constructor(props: ConnectIframeDefinitions.Props) { super(props); this.state = { width: props.width, height: props.height, loadingState: LoadingState.INIT, hostFrameOffset: props.options.hostFrameOffset || 1 }; this.iframeContext = { url: props.url, appKey: props.appKey, moduleKey: props.moduleKey, options: props.options } as ConnectIframeContext; } resize = (width: string, height: string): void => { this.setState({width: width, height: height}); } sizeToParent = (): void => { this.setState({width: '100%', height: '100%'}); } registerUnmountCallback = (callback: Function): void => { this.unmountCallbacks.push(callback); } hideInlineDialog = (): void => { if (this.props.connectIframeProvider.onHideInlineDialog) { this.props.connectIframeProvider.onHideInlineDialog(); } } getId = (): string|null => { if (this.iframeAttributes) { return this.iframeAttributes.id; } else { return null; } } getIFrameLifecycleEventManager = (): IFrameLifecycleEventManager | undefined => { return this.iframeLifecycleEventManager; } iframeEstablishedCallback = (): void => { this.setState({loadingState: LoadingState.LOADED}); } iframeFailedToLoadCallback = (): void => { this.setState({loadingState: LoadingState.FAILED}); } iframeTimeoutCallback = (): void => { this.setState({loadingState: LoadingState.TIMEOUT}); } _createIFrameLifecycleManager = (): void => { this.iframeLifecycleEventManager = new IFrameLifecycleEventManager(this); } _unregisterIFrameLifecycleManager = (): void => { if (this.iframeLifecycleEventManager) { this.iframeLifecycleEventManager.unregister(this); } } _createExtension = (): void => { const simpleXdmExtension = this.props.connectHost.createExtension({ addon_key: this.iframeContext.appKey, key: this.iframeContext.moduleKey, url: this.iframeContext.url, options: Object.assign({}, this.iframeContext.options, { resize: this.resize.bind(this), sizeToParent: this.sizeToParent.bind(this), registerUnmountCallback: this.registerUnmountCallback.bind(this), _contextualOperations: { hideInlineDialog: this.hideInlineDialog.bind(this) } }) }); this.iframeAttributes = simpleXdmExtension.iframeAttributes; logger.debug('Created iframe for add-on ', this.iframeContext.appKey, this.iframeAttributes); analytics.trigger( AnalyticsCategories.iframe, analytics.markAsSafe(...allowedStrings)(AnalyticsActions.create), analytics.dangerouslyCreateSafeString(this.iframeContext.appKey), {} ); } _destroyExtension = (): void => { if (this.iframeAttributes) { this.props.connectHost.destroy(this.iframeAttributes.id); } } _isSubIframe = (): boolean => { return (this.state.hostFrameOffset && this.state.hostFrameOffset > 1) as boolean; } _initialise = (): void => { if (this.props.children && this.props.connectIframeProvider.onStoreNestedIframeJSON) { const childComponent = this.props.children as React.Component; const childProps = childComponent.props as ConnectIframeDefinitions.Props; this.props.connectIframeProvider.onStoreNestedIframeJSON(childProps); } if (this._isSubIframe()) { this.setState({loadingState: LoadingState.LOADING}); return; } this._createIFrameLifecycleManager(); if (!this.props.connectIframeProvider.resolveIframeContext) { this._createExtension(); this.setState({loadingState: LoadingState.LOADING}); return; } this.props.connectIframeProvider.resolveIframeContext(this.iframeContext, this.props.connectHost) .then(iframeContext => { const propsMutated = (iframeContext.appKey !== this.props.appKey) || (iframeContext.moduleKey !== this.props.moduleKey); if (propsMutated) { logger.warn('Iframe context changed during resolution'); return; } this.iframeContext = iframeContext; this._createExtension(); this.setState({loadingState: LoadingState.LOADING}); }); } componentWillReceiveProps(nextProps: ConnectIframeDefinitions.Props): void { if ( (this.props.appKey !== nextProps.appKey) || (this.props.moduleKey !== nextProps.moduleKey) ){ this._unregisterIFrameLifecycleManager(); this._destroyExtension(); this.setState({ width: nextProps.width, height: nextProps.height, hostFrameOffset: nextProps.options.hostFrameOffset || 1, loadingState: LoadingState.INIT }); } } componentWillUnmount(): void { this.unmountCallbacks.forEach(cb => cb()); this._unregisterIFrameLifecycleManager(); this._destroyExtension(); } render(): React.ReactElement | null { switch (this.state.loadingState) { case LoadingState.LOADED: case LoadingState.LOADING: case LoadingState.TIMEOUT: let iframeStyles; // Ask the product for the styles to use... if (this.props.connectIframeProvider.buildIframeStyles) { iframeStyles = this.props.connectIframeProvider.buildIframeStyles(this.state.loadingState, LoadingState); } // if iframeStyles is undefined and loadingState is loading/timeout, set the default style if (!iframeStyles && (this.state.loadingState === LoadingState.LOADING || this.state.loadingState === LoadingState.TIMEOUT)) { iframeStyles = {opacity: 0.0}; } return ( {this.state.loadingState === LoadingState.TIMEOUT ? : null } {this.state.loadingState === LoadingState.LOADING ? : null } {this._isSubIframe() ? this.props.children : } ); case LoadingState.FAILED: return
; case LoadingState.INIT: case LoadingState.RESOLVING: default: return null; } } componentDidMount(): void { if (this.state.loadingState === LoadingState.INIT) { this.setState({loadingState: LoadingState.RESOLVING}); this._initialise(); } } componentDidUpdate(): void { if (this.state.loadingState === LoadingState.INIT) { this.setState({loadingState: LoadingState.RESOLVING}); this._initialise(); } } } export {ConnectIframeDefinitions, ConnectIframe};