Refactor postMessage service

This commit is contained in:
Chi Vinh Le
2018-02-26 22:10:01 +01:00
parent 1b05f146fe
commit 17a0aebdf7
12 changed files with 159 additions and 140 deletions
+7 -2
View File
@@ -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.
@@ -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
+2 -3
View File
@@ -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
@@ -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;
+1
View File
@@ -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';
@@ -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 <WrappedComponent {...this.props} />;
}
}
return WithPopupAuthHandler;
});
+5 -1
View File
@@ -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.
@@ -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';
@@ -1,2 +0,0 @@
export { default as sendMessage } from './sendMessage';
export { subscribeToMessages, unsubscribeFromMessages } from './subscriptions';
@@ -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);
};
@@ -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]);
};
@@ -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]);
},
};
}