From 17a0aebdf7af3e47682c9ebe8dc1762dc4a54176 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 26 Feb 2018 22:10:01 +0100 Subject: [PATCH] Refactor postMessage service --- client/coral-auth-callback/src/index.js | 9 +- .../src/containers/Embed.js | 40 +-------- client/coral-framework/actions/auth.js | 5 +- .../components/TalkProvider.js | 2 + client/coral-framework/hocs/index.js | 1 + .../hocs/withPopupAuthHandler.js | 54 ++++++++++++ client/coral-framework/services/bootstrap.js | 6 +- .../services/messages/constants.js | 11 --- .../services/messages/index.js | 2 - .../services/messages/sendMessage.js | 13 --- .../services/messages/subscriptions.js | 70 --------------- .../coral-framework/services/postMessage.js | 86 +++++++++++++++++++ 12 files changed, 159 insertions(+), 140 deletions(-) create mode 100644 client/coral-framework/hocs/withPopupAuthHandler.js delete mode 100644 client/coral-framework/services/messages/constants.js delete mode 100644 client/coral-framework/services/messages/index.js delete mode 100644 client/coral-framework/services/messages/sendMessage.js delete mode 100644 client/coral-framework/services/messages/subscriptions.js create mode 100644 client/coral-framework/services/postMessage.js diff --git a/client/coral-auth-callback/src/index.js b/client/coral-auth-callback/src/index.js index 204a86470..41a83dcd2 100644 --- a/client/coral-auth-callback/src/index.js +++ b/client/coral-auth-callback/src/index.js @@ -1,8 +1,13 @@ import { HANDLE_SUCCESSFUL_LOGIN } from 'coral-framework/constants/auth'; -import sendMessage from 'coral-framework/services/messages/sendMessage'; +import { getStaticConfiguration } from 'coral-framework/services/staticConfiguration'; +import { createPostMessage } from 'coral-framework/services/postMessage'; document.addEventListener('DOMContentLoaded', () => { try { + const staticConfig = getStaticConfiguration(); + const { STATIC_ORIGIN: origin } = staticConfig; + const postMessage = createPostMessage(origin); + // Get the auth element and parse it as JSON by decoding it. const auth = document.getElementById('auth'); const doc = document.implementation.createHTMLDocument(''); @@ -18,7 +23,7 @@ document.addEventListener('DOMContentLoaded', () => { const { user, token } = data; // Send the state back. - sendMessage(HANDLE_SUCCESSFUL_LOGIN, { user, token }); + postMessage.post(HANDLE_SUCCESSFUL_LOGIN, { user, token }); } } finally { // Always close the window. diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index 812c18e22..b9e0cd357 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -19,7 +19,7 @@ import { getDefinitionName, getSlotFragmentSpreads, } from 'coral-framework/utils'; -import { withQuery } from 'coral-framework/hocs'; +import { withQuery, withPopupAuthHandler } from 'coral-framework/hocs'; import Embed from '../components/Embed'; import Stream from '../tabs/stream/containers/Stream'; import AutomaticAssetClosure from './AutomaticAssetClosure'; @@ -29,17 +29,6 @@ import t from 'coral-framework/services/i18n'; import PropTypes from 'prop-types'; import { setActiveTab } from '../actions/embed'; -// TODO: refactor out to a HOC -import { HANDLE_SUCCESSFUL_LOGIN } from 'coral-framework/constants/auth'; -import { - handleSuccessfulLogin, - setAuthToken, -} from 'coral-framework/actions/auth'; -import { - subscribeToMessages, - unsubscribeFromMessages, -} from 'coral-framework/services/messages'; - class EmbedContainer extends React.Component { static contextTypes = { pym: PropTypes.object, @@ -48,8 +37,6 @@ class EmbedContainer extends React.Component { subscriptions = []; subscribeToUpdates(props = this.props) { - subscribeToMessages(this.handleAuth); - if (props.currentUser) { const newSubscriptions = [ { @@ -99,28 +86,8 @@ class EmbedContainer extends React.Component { unsubscribe() { this.subscriptions.forEach(unsubscribe => unsubscribe()); this.subscriptions = []; - unsubscribeFromMessages(this.handleAuth); } - // TODO: refactor out to a HOC - handleAuth = ({ name, data }) => { - if (name !== HANDLE_SUCCESSFUL_LOGIN) { - return; - } - - // data will contain the user and token. - const { user, token } = data; - const { handleSuccessfulLogin, setAuthToken } = this.props; - - if (user && token) { - handleSuccessfulLogin(user, token); - } else if (token) { - setAuthToken(token); - } else { - console.error('Invalid auth data supplied', data); - } - }; - resubscribe(props) { this.unsubscribe(); this.subscribeToUpdates(props); @@ -334,8 +301,6 @@ EmbedContainer.propTypes = { fetchAssetSuccess: PropTypes.func, showSignInDialog: PropTypes.bool, signInDialogFocus: PropTypes.bool, - handleSuccessfulLogin: PropTypes.func.isRequired, - setAuthToken: PropTypes.func.isRequired, }; const mapStateToProps = state => ({ @@ -363,13 +328,12 @@ const mapDispatchToProps = dispatch => blurSignInDialog, hideSignInDialog, updateStatus, - handleSuccessfulLogin, - setAuthToken, }, dispatch ); export default compose( + withPopupAuthHandler, connect(mapStateToProps, mapDispatchToProps), branch(props => !props.checkedInitialLogin, renderComponent(Spinner)), withEmbedQuery diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 6fe8ad508..17b1c38f9 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -1,6 +1,5 @@ import * as actions from '../constants/auth'; import jwtDecode from 'jwt-decode'; -import { sendMessage } from '../services/messages'; function cleanAuthData(localStorage) { localStorage.removeItem('token'); @@ -64,7 +63,7 @@ export const setAuthToken = token => (dispatch, _, { localStorage }) => { export const handleSuccessfulLogin = (user, token) => ( dispatch, _, - { client, localStorage } + { client, localStorage, postMessage } ) => { const { exp } = jwtDecode(token); @@ -76,7 +75,7 @@ export const handleSuccessfulLogin = (user, token) => ( // Send the message via the messages service to the window.opener if it // exists. if (window.opener) { - sendMessage( + postMessage.post( actions.HANDLE_SUCCESSFUL_LOGIN, { user, token }, window.opener diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index 3164d8e9d..343d559ea 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -17,6 +17,7 @@ class TalkProvider extends React.Component { store: this.props.store, pymLocalStorage: this.props.pymLocalStorage, pymSessionStorage: this.props.pymSessionStorage, + postMessage: this.props.postMessage, }; } @@ -39,6 +40,7 @@ TalkProvider.childContextTypes = { pymSessionStorage: PropTypes.object, history: PropTypes.object, store: PropTypes.object, + postMessage: PropTypes.object, }; TalkProvider.propTypes = TalkProvider.childContextTypes; diff --git a/client/coral-framework/hocs/index.js b/client/coral-framework/hocs/index.js index eb481de99..5e2e3dd40 100644 --- a/client/coral-framework/hocs/index.js +++ b/client/coral-framework/hocs/index.js @@ -10,6 +10,7 @@ export { default as withSignIn } from './withSignIn'; export { default as withSignUp } from './withSignUp'; export { default as withForgotPassword } from './withForgotPassword'; export { default as withSetUsername } from './withSetUsername'; +export { default as withPopupAuthHandler } from './withPopupAuthHandler'; export { default as withResendEmailConfirmation, } from './withResendEmailConfirmation'; diff --git a/client/coral-framework/hocs/withPopupAuthHandler.js b/client/coral-framework/hocs/withPopupAuthHandler.js new file mode 100644 index 000000000..ab2530e66 --- /dev/null +++ b/client/coral-framework/hocs/withPopupAuthHandler.js @@ -0,0 +1,54 @@ +import React from 'react'; +import hoistStatics from 'recompose/hoistStatics'; +import PropTypes from 'prop-types'; +import { HANDLE_SUCCESSFUL_LOGIN } from 'coral-framework/constants/auth'; +import { + handleSuccessfulLogin, + setAuthToken, +} from 'coral-framework/actions/auth'; + +/** + * WithPopupAuthHandler listens to successful logins over + * the `postMessage` service. + */ +export default hoistStatics(WrappedComponent => { + class WithPopupAuthHandler extends React.Component { + static contextTypes = { + store: PropTypes.object, + postMessage: PropTypes.object, + }; + + constructor(props, context) { + super(props, context); + context.postMessage.subscribe(this.handleAuth); + } + + componentWillUnmount() { + this.context.postMessage.unsubscribe(this.handleAuth); + } + + handleAuth = ({ name, data }) => { + if (name !== HANDLE_SUCCESSFUL_LOGIN) { + return; + } + + const { store } = this.context; + // data will contain the user and token. + const { user, token } = data; + + if (user && token) { + store.dispatch(handleSuccessfulLogin(user, token)); + } else if (token) { + store.dispatch(setAuthToken(token)); + } else { + console.error('Invalid auth data supplied', data); + } + }; + + render() { + return ; + } + } + + return WithPopupAuthHandler; +}); diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index a7dd6df98..691404369 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -14,6 +14,7 @@ import { createPluginsService } from './plugins'; import { createNotificationService } from './notification'; import { createGraphQLRegistry } from './graphqlRegistry'; import { createGraphQLService } from './graphql'; +import { createPostMessage } from './postMessage'; import globalFragments from 'coral-framework/graphql/fragments'; import { createStorage, @@ -118,7 +119,7 @@ export async function createContext({ }); const staticConfig = getStaticConfiguration(); - let { LIVE_URI: liveUri } = staticConfig; + let { LIVE_URI: liveUri, STATIC_ORIGIN: origin } = staticConfig; if (liveUri == null) { // The protocol must match the origin protocol, secure/insecure. const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; @@ -128,6 +129,8 @@ export async function createContext({ liveUri = `${protocol}://${location.host}${BASE_PATH}api/v1/live`; } + const postMessage = createPostMessage(origin); + const client = createClient({ uri: `${BASE_PATH}api/v1/graph/ql`, liveUri, @@ -158,6 +161,7 @@ export async function createContext({ pymLocalStorage, pymSessionStorage, inIframe, + postMessage, }; // Load framework fragments. diff --git a/client/coral-framework/services/messages/constants.js b/client/coral-framework/services/messages/constants.js deleted file mode 100644 index 7204194f7..000000000 --- a/client/coral-framework/services/messages/constants.js +++ /dev/null @@ -1,11 +0,0 @@ -import { getStaticConfiguration } from 'coral-framework/services/staticConfiguration'; - -// Load the static url from the static configuration, we'll use it to formulate -// the authorized domain to allow the message to be sent to. -const { STATIC_ORIGIN } = getStaticConfiguration(); - -// Export the origin that will be sent. -export { STATIC_ORIGIN as ORIGIN }; - -// Scope all the requests made via the messages service. -export const SCOPE = 'client'; diff --git a/client/coral-framework/services/messages/index.js b/client/coral-framework/services/messages/index.js deleted file mode 100644 index 311034624..000000000 --- a/client/coral-framework/services/messages/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as sendMessage } from './sendMessage'; -export { subscribeToMessages, unsubscribeFromMessages } from './subscriptions'; diff --git a/client/coral-framework/services/messages/sendMessage.js b/client/coral-framework/services/messages/sendMessage.js deleted file mode 100644 index a0e7dd803..000000000 --- a/client/coral-framework/services/messages/sendMessage.js +++ /dev/null @@ -1,13 +0,0 @@ -import { SCOPE, ORIGIN } from './constants'; - -export default (name, data, target = window.opener) => { - if (!target) { - return; - } - - // Serialize the message to be sent via postMessage. - const msg = { name, data, scope: SCOPE }; - - // Send the message. - target.postMessage(msg, ORIGIN); -}; diff --git a/client/coral-framework/services/messages/subscriptions.js b/client/coral-framework/services/messages/subscriptions.js deleted file mode 100644 index 984a8377d..000000000 --- a/client/coral-framework/services/messages/subscriptions.js +++ /dev/null @@ -1,70 +0,0 @@ -import { SCOPE, ORIGIN } from './constants'; - -import set from 'lodash/set'; -import has from 'lodash/has'; -import get from 'lodash/get'; -import remove from 'lodash/remove'; - -// withOriginCheck wraps a given handler for a messages event with a check to -// see if the origin matches. -const withOriginCheck = handler => { - return event => { - const origin = get(event, 'origin'); - if (origin !== ORIGIN) { - return; - } - - const scope = get(event, 'data.scope'); - if (scope !== SCOPE) { - return; - } - - // Pass the handler the event details. - handler(event); - }; -}; - -// withParseData will parse the data. -const withParseData = handler => { - return event => { - const data = get(event, 'data.data'); - const name = get(event, 'data.name'); - if (!data || !name) { - return; - } - - handler({ data, name, event }); - }; -}; - -// Store a reference to each listener added. -const listeners = {}; - -export const subscribeToMessages = (handler, target = window) => { - // If this handler is already attached to the target, detach it. - if (has(listeners, [target, handler])) { - unsubscribeFromMessages(handler, target); - } - - // Wrap the listener with a origin check. - const listener = withOriginCheck(withParseData(handler)); - - // Save a reference to the compiled listener. - set(listeners, [target, handler], listener); - - // Attach the listener to the target. - target.addEventListener('message', listener); -}; - -export const unsubscribeFromMessages = (handler, target = window) => { - if (!has(listeners, [target, handler])) { - return; - } - - const listener = get(listeners, [target, handler]); - - // Remove the listener from the target. - target.removeEventListener('message', listener); - - remove(listeners, [target, handler]); -}; diff --git a/client/coral-framework/services/postMessage.js b/client/coral-framework/services/postMessage.js new file mode 100644 index 000000000..bf4a11305 --- /dev/null +++ b/client/coral-framework/services/postMessage.js @@ -0,0 +1,86 @@ +import set from 'lodash/set'; +import has from 'lodash/has'; +import get from 'lodash/get'; +import remove from 'lodash/remove'; + +/** + * createPostMessage returns a service that deals with cross + * window communication using the postMessage API. + * @param {Object} options + * @return {Object} messenger service + */ +export function createPostMessage(origin, scope = 'client') { + // Store a reference to each listener added. + const listeners = {}; + + // withOriginCheck wraps a given handler for a messages event with a check to + // see if the origin matches. + const withOriginCheck = handler => { + return event => { + if (get(event, 'origin') !== origin) { + return; + } + + if (get(event, 'data.scope') !== scope) { + return; + } + + // Pass the handler the event details. + handler(event); + }; + }; + + // withParseData will parse the data. + const withParseData = handler => { + return event => { + const data = get(event, 'data.data'); + const name = get(event, 'data.name'); + if (!data || !name) { + return; + } + + handler({ data, name, event }); + }; + }; + + return { + post(name, data, target = window.opener) { + if (!target) { + return; + } + + // Serialize the message to be sent via postMessage. + const msg = { name, data, scope }; + + // Send the message. + target.postMessage(msg, origin); + }, + subscribe: (handler, target = window) => { + // If this handler is already attached to the target, detach it. + if (has(listeners, [target, handler])) { + this.unsubscribeFromMessages(handler, target); + } + + // Wrap the listener with a origin check. + const listener = withOriginCheck(withParseData(handler)); + + // Save a reference to the compiled listener. + set(listeners, [target, handler], listener); + + // Attach the listener to the target. + target.addEventListener('message', listener); + }, + unsubscribe: (handler, target = window) => { + if (!has(listeners, [target, handler])) { + return; + } + + const listener = get(listeners, [target, handler]); + + // Remove the listener from the target. + target.removeEventListener('message', listener); + + remove(listeners, [target, handler]); + }, + }; +}