mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 19:06:38 +08:00
[next] Start a clean session when user logs in / out (#1853)
* Clear user session after login / logout * Filename cases * Improve type checking * Apply suggestions
This commit is contained in:
@@ -2,11 +2,7 @@ import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
TalkContext,
|
||||
TalkContextProvider,
|
||||
} from "talk-framework/lib/bootstrap";
|
||||
import { createManaged } from "talk-framework/lib/bootstrap";
|
||||
|
||||
import AppContainer from "./containers/AppContainer";
|
||||
import resizePopup from "./dom/resizePopup";
|
||||
@@ -32,23 +28,17 @@ function pollPopupHeight(interval: number = 100) {
|
||||
}, interval);
|
||||
}
|
||||
|
||||
// This is called when the context is first initialized.
|
||||
async function init({ relayEnvironment }: TalkContext) {
|
||||
await initLocalState(relayEnvironment);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Bootstrap our context.
|
||||
const context = await createContext({
|
||||
init,
|
||||
const ManagedTalkContextProvider = await createManaged({
|
||||
initLocalState,
|
||||
localesData,
|
||||
userLocales: navigator.languages,
|
||||
});
|
||||
|
||||
const Index: StatelessComponent = () => (
|
||||
<TalkContextProvider value={context}>
|
||||
<ManagedTalkContextProvider>
|
||||
<AppContainer />
|
||||
</TalkContextProvider>
|
||||
</ManagedTalkContextProvider>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Index />, document.getElementById("app"));
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { IResolvers } from "graphql-tools";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
import TestRenderer from "react-test-renderer";
|
||||
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
|
||||
@@ -29,7 +31,6 @@ export default function create(params: CreateParams) {
|
||||
logNetwork: params.logNetwork,
|
||||
resolvers: params.resolvers,
|
||||
initLocalState: (localRecord, source, env) => {
|
||||
localRecord.setValue(0, "authRevision");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, env);
|
||||
}
|
||||
@@ -45,6 +46,8 @@ export default function create(params: CreateParams) {
|
||||
postMessage: new PostMessageService(),
|
||||
browserInfo: { ios: false },
|
||||
uuidGenerator: createUUIDGenerator(),
|
||||
eventEmitter: new EventEmitter2({ wildcard: true, maxListeners: 20 }),
|
||||
clearSession: noop,
|
||||
};
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
|
||||
@@ -3,12 +3,9 @@ import { Environment, ROOT_ID } from "relay-runtime";
|
||||
export default function getMe(environment: Environment) {
|
||||
const source = environment.getStore().getSource();
|
||||
const root = source.get(ROOT_ID)!;
|
||||
const meKey = Object.keys(root)
|
||||
.reverse()
|
||||
.find(s => s.startsWith("me("))!;
|
||||
if (!root[meKey]) {
|
||||
if (!root.me) {
|
||||
return null;
|
||||
}
|
||||
const meID = root[meKey].__ref;
|
||||
const meID = root.me.__ref;
|
||||
return source.get(meID)!;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { LocalizationProvider } from "fluent-react/compat";
|
||||
import { FluentBundle } from "fluent/compat";
|
||||
import { Child as PymChild } from "pym.js";
|
||||
@@ -52,6 +53,12 @@ export interface TalkContext {
|
||||
|
||||
/** Generates uuids. */
|
||||
uuidGenerator: () => string;
|
||||
|
||||
/** A event emitter */
|
||||
eventEmitter: EventEmitter2;
|
||||
|
||||
/** Clear session data. */
|
||||
clearSession: () => void;
|
||||
}
|
||||
|
||||
const { Provider, Consumer } = React.createContext<TalkContext>({} as any);
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import { noop } from "lodash";
|
||||
import { Child as PymChild } from "pym.js";
|
||||
import React from "react";
|
||||
import { Formatter } from "react-timeago";
|
||||
import { Environment, Network, RecordSource, Store } from "relay-runtime";
|
||||
import uuid from "uuid/v4";
|
||||
|
||||
import { getBrowserInfo } from "talk-framework/lib/browserInfo";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import {
|
||||
createLocalStorage,
|
||||
createPromisifiedStorage,
|
||||
createPymStorage,
|
||||
createSessionStorage,
|
||||
} from "talk-framework/lib/storage";
|
||||
|
||||
import { RestClient } from "talk-framework/lib/rest";
|
||||
import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
|
||||
|
||||
import { generateBundles, LocalesData, negotiateLanguages } from "../i18n";
|
||||
import { createFetch, TokenGetter } from "../network";
|
||||
import { PostMessageService } from "../postMessage";
|
||||
import { TalkContext } from "./TalkContext";
|
||||
|
||||
interface CreateContextArguments {
|
||||
/** Locales that the user accepts, usually `navigator.languages`. */
|
||||
userLocales: ReadonlyArray<string>;
|
||||
|
||||
/** Locales data that is returned by our `locales-loader`. */
|
||||
localesData: LocalesData;
|
||||
|
||||
/** Init will be called after the context has been created. */
|
||||
init?: ((context: TalkContext) => void | Promise<void>);
|
||||
|
||||
/** A pym child that interacts with the pym parent. */
|
||||
pym?: PymChild;
|
||||
|
||||
/** Supports emitting and listening to events. */
|
||||
eventEmitter?: EventEmitter2;
|
||||
}
|
||||
|
||||
/**
|
||||
* timeagoFormatter integrates timeago into our translation
|
||||
* framework. It gets injected into the UIContext.
|
||||
*/
|
||||
export const timeagoFormatter: Formatter = (value, unit, suffix) => {
|
||||
// We use 'in' instead of 'from now' for language consistency
|
||||
const ourSuffix = suffix === "from now" ? "in" : suffix;
|
||||
return (
|
||||
<Localized
|
||||
id="framework-timeago"
|
||||
$value={value}
|
||||
$unit={unit}
|
||||
$suffix={ourSuffix}
|
||||
>
|
||||
<span>now</span>
|
||||
</Localized>
|
||||
);
|
||||
};
|
||||
|
||||
function areWeInIframe() {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `createContext` manages the dependencies of our framework
|
||||
* and returns a `TalkContext` that can be passed to the
|
||||
* `TalkContextProvider`.
|
||||
*/
|
||||
export default async function createContext({
|
||||
init = noop,
|
||||
userLocales,
|
||||
localesData,
|
||||
pym,
|
||||
eventEmitter = new EventEmitter2({ wildcard: true }),
|
||||
}: CreateContextArguments): Promise<TalkContext> {
|
||||
const inIframe = areWeInIframe();
|
||||
// Initialize Relay.
|
||||
const source = new RecordSource();
|
||||
const tokenGetter: TokenGetter = () => {
|
||||
const localState = source.get(LOCAL_ID);
|
||||
if (localState) {
|
||||
return localState.authToken || "";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
const relayEnvironment = new Environment({
|
||||
network: Network.create(createFetch(tokenGetter)),
|
||||
store: new Store(source),
|
||||
});
|
||||
|
||||
// Listen for outside clicks.
|
||||
let registerClickFarAway: ClickFarAwayRegister | undefined;
|
||||
if (pym) {
|
||||
registerClickFarAway = cb => {
|
||||
pym.onMessage("click", cb);
|
||||
// Return unlisten callback.
|
||||
return () => {
|
||||
const index = pym.messageHandlers.click.indexOf(cb);
|
||||
if (index > -1) {
|
||||
pym.messageHandlers.click.splice(index, 1);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize i18n.
|
||||
const locales = negotiateLanguages(userLocales, localesData);
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
// tslint:disable:next-line: no-console
|
||||
console.log(`Negotiated locales ${JSON.stringify(locales)}`);
|
||||
}
|
||||
|
||||
const localeBundles = await generateBundles(locales, localesData);
|
||||
|
||||
// Assemble context.
|
||||
const context = {
|
||||
relayEnvironment,
|
||||
localeBundles,
|
||||
timeagoFormatter,
|
||||
pym,
|
||||
eventEmitter,
|
||||
registerClickFarAway,
|
||||
rest: new RestClient("/api", tokenGetter),
|
||||
postMessage: new PostMessageService(),
|
||||
localStorage:
|
||||
(pym && inIframe && createPymStorage(pym, "localStorage")) ||
|
||||
createPromisifiedStorage(createLocalStorage()),
|
||||
sessionStorage:
|
||||
(pym && inIframe && createPymStorage(pym, "sessionStorage")) ||
|
||||
createPromisifiedStorage(createSessionStorage()),
|
||||
browserInfo: getBrowserInfo(),
|
||||
uuidGenerator: uuid,
|
||||
};
|
||||
|
||||
// Run custom initializations.
|
||||
await init(context);
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import { noop } from "lodash";
|
||||
import { Child as PymChild } from "pym.js";
|
||||
import React, { Component, ComponentType } from "react";
|
||||
import { Formatter } from "react-timeago";
|
||||
import { Environment, Network, RecordSource, Store } from "relay-runtime";
|
||||
import uuid from "uuid/v4";
|
||||
|
||||
import { getBrowserInfo } from "talk-framework/lib/browserInfo";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import {
|
||||
createLocalStorage,
|
||||
createPromisifiedStorage,
|
||||
createPymStorage,
|
||||
createSessionStorage,
|
||||
PromisifiedStorage,
|
||||
} from "talk-framework/lib/storage";
|
||||
|
||||
import { RestClient } from "talk-framework/lib/rest";
|
||||
import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
|
||||
|
||||
import { generateBundles, LocalesData, negotiateLanguages } from "../i18n";
|
||||
import { createFetch, TokenGetter } from "../network";
|
||||
import { PostMessageService } from "../postMessage";
|
||||
import { TalkContext, TalkContextProvider } from "./TalkContext";
|
||||
|
||||
export type InitLocalState = ((
|
||||
environment: Environment,
|
||||
context: TalkContext
|
||||
) => void | Promise<void>);
|
||||
|
||||
interface CreateContextArguments {
|
||||
/** Locales that the user accepts, usually `navigator.languages`. */
|
||||
userLocales: ReadonlyArray<string>;
|
||||
|
||||
/** Locales data that is returned by our `locales-loader`. */
|
||||
localesData: LocalesData;
|
||||
|
||||
/** Init will be called after the context has been created. */
|
||||
initLocalState?: InitLocalState;
|
||||
|
||||
/** A pym child that interacts with the pym parent. */
|
||||
pym?: PymChild;
|
||||
|
||||
/** Supports emitting and listening to events. */
|
||||
eventEmitter?: EventEmitter2;
|
||||
}
|
||||
|
||||
/**
|
||||
* timeagoFormatter integrates timeago into our translation
|
||||
* framework. It gets injected into the UIContext.
|
||||
*/
|
||||
export const timeagoFormatter: Formatter = (value, unit, suffix) => {
|
||||
// We use 'in' instead of 'from now' for language consistency
|
||||
const ourSuffix = suffix === "from now" ? "in" : suffix;
|
||||
return (
|
||||
<Localized
|
||||
id="framework-timeago"
|
||||
$value={value}
|
||||
$unit={unit}
|
||||
$suffix={ourSuffix}
|
||||
>
|
||||
<span>now</span>
|
||||
</Localized>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if we are in an iframe.
|
||||
*/
|
||||
function areWeInIframe() {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function createRelayEnvironment() {
|
||||
// Initialize Relay.
|
||||
const source = new RecordSource();
|
||||
const tokenGetter: TokenGetter = () => {
|
||||
const localState = source.get(LOCAL_ID);
|
||||
if (localState) {
|
||||
return localState.authToken || "";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
const environment = new Environment({
|
||||
network: Network.create(createFetch(tokenGetter)),
|
||||
store: new Store(source),
|
||||
});
|
||||
return { environment, tokenGetter };
|
||||
}
|
||||
|
||||
function createRestAPI(tokenGetter: (() => string)) {
|
||||
return new RestClient("/api", tokenGetter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a managed TalkContextProvider, that includes given context
|
||||
* and handles context changes, e.g. when a user session changes.
|
||||
*/
|
||||
function createMangedTalkContextProvider(
|
||||
context: TalkContext,
|
||||
initLocalState: InitLocalState
|
||||
) {
|
||||
const ManagedTalkContextProvider = class extends Component<
|
||||
{},
|
||||
{ context: TalkContext }
|
||||
> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
this.state = {
|
||||
context: {
|
||||
...context,
|
||||
clearSession: this.clearSession,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// This is called every time a user session starts or ends.
|
||||
private clearSession = async () => {
|
||||
// Clear session storage.
|
||||
this.state.context.sessionStorage.clear();
|
||||
|
||||
// Create a new context with a new Relay Environment.
|
||||
const {
|
||||
environment: newEnvironment,
|
||||
tokenGetter: newTokenGetter,
|
||||
} = createRelayEnvironment();
|
||||
|
||||
const newContext = {
|
||||
...this.state.context,
|
||||
relayEnvironment: newEnvironment,
|
||||
rest: createRestAPI(newTokenGetter),
|
||||
};
|
||||
|
||||
// Initialize local state.
|
||||
await initLocalState(newContext.relayEnvironment, newContext);
|
||||
|
||||
// Propagate new context.
|
||||
this.setState({
|
||||
context: newContext,
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<TalkContextProvider value={this.state.context}>
|
||||
{this.props.children}
|
||||
</TalkContextProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return ManagedTalkContextProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* resolveLocalStorage decides which local storage to use in the context
|
||||
*/
|
||||
function resolveLocalStorage(pym?: PymChild): PromisifiedStorage {
|
||||
if (pym && areWeInIframe()) {
|
||||
// Use local storage over pym when we have pym and are in an iframe.
|
||||
return createPymStorage(pym, "localStorage");
|
||||
}
|
||||
// Use promisified, prefixed local storage.
|
||||
return createPromisifiedStorage(createLocalStorage());
|
||||
}
|
||||
|
||||
/**
|
||||
* resolveSessionStorage decides which session storage to use in the context
|
||||
*/
|
||||
function resolveSessionStorage(pym?: PymChild): PromisifiedStorage {
|
||||
if (pym && areWeInIframe()) {
|
||||
// Use session storage over pym when we have pym and are in an iframe.
|
||||
return createPymStorage(pym, "sessionStorage");
|
||||
}
|
||||
// Use promisified, prefixed session storage.
|
||||
return createPromisifiedStorage(createSessionStorage());
|
||||
}
|
||||
|
||||
/**
|
||||
* `createManaged` establishes the dependencies of our framework
|
||||
* and returns a `ManagedTalkContextProvider` that provides the context
|
||||
* to the rest of the application.
|
||||
*/
|
||||
export default async function createManaged({
|
||||
initLocalState = noop,
|
||||
userLocales,
|
||||
localesData,
|
||||
pym,
|
||||
eventEmitter = new EventEmitter2({ wildcard: true, maxListeners: 20 }),
|
||||
}: CreateContextArguments): Promise<ComponentType> {
|
||||
// Listen for outside clicks.
|
||||
let registerClickFarAway: ClickFarAwayRegister | undefined;
|
||||
if (pym) {
|
||||
registerClickFarAway = cb => {
|
||||
pym.onMessage("click", cb);
|
||||
// Return unlisten callback.
|
||||
return () => {
|
||||
const index = pym.messageHandlers.click.indexOf(cb);
|
||||
if (index > -1) {
|
||||
pym.messageHandlers.click.splice(index, 1);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize i18n.
|
||||
const locales = negotiateLanguages(userLocales, localesData);
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
// tslint:disable:next-line: no-console
|
||||
console.log(`Negotiated locales ${JSON.stringify(locales)}`);
|
||||
}
|
||||
|
||||
const localeBundles = await generateBundles(locales, localesData);
|
||||
|
||||
const localStorage = resolveLocalStorage(pym);
|
||||
const sessionStorage = resolveSessionStorage(pym);
|
||||
|
||||
const { environment, tokenGetter } = createRelayEnvironment();
|
||||
|
||||
// Assemble context.
|
||||
const context: TalkContext = {
|
||||
relayEnvironment: environment,
|
||||
localeBundles,
|
||||
timeagoFormatter,
|
||||
pym,
|
||||
eventEmitter,
|
||||
registerClickFarAway,
|
||||
rest: createRestAPI(tokenGetter),
|
||||
postMessage: new PostMessageService(),
|
||||
localStorage,
|
||||
sessionStorage,
|
||||
browserInfo: getBrowserInfo(),
|
||||
uuidGenerator: uuid,
|
||||
// Noop, this is later replaced by the
|
||||
// managed TalkContextProvider.
|
||||
clearSession: noop,
|
||||
};
|
||||
|
||||
// Initialize local state.
|
||||
await initLocalState(context.relayEnvironment, context);
|
||||
|
||||
// Returns a managed TalkContextProvider, that includes the above
|
||||
// context and handles context changes, e.g. when a user session changes.
|
||||
return createMangedTalkContextProvider(context, initLocalState);
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
export * from "./TalkContext";
|
||||
export { default as createContext } from "./createContext";
|
||||
export {
|
||||
TalkContext,
|
||||
TalkContextConsumer,
|
||||
TalkContextProvider,
|
||||
} from "./TalkContext";
|
||||
export { default as createManaged } from "./createManaged";
|
||||
export { default as withContext } from "./withContext";
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
commitLocalUpdate,
|
||||
Environment,
|
||||
RecordSourceProxy,
|
||||
} from "relay-runtime";
|
||||
|
||||
export default function commitLocalUpdatePromisified(
|
||||
environment: Environment,
|
||||
updater: (store: RecordSourceProxy) => Promise<void>
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
commitLocalUpdate(environment, store => {
|
||||
updater(store)
|
||||
.then(() => resolve())
|
||||
.catch(err => reject(err));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -13,3 +13,6 @@ export {
|
||||
commitMutationPromiseNormalized,
|
||||
} from "./commitMutationPromise";
|
||||
export { graphql } from "react-relay";
|
||||
export {
|
||||
default as commitLocalUpdatePromisified,
|
||||
} from "./commitLocalUpdatePromisified";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { createInMemoryStorage } from "talk-framework/lib/storage";
|
||||
import { createPromisifiedStorage } from "talk-framework/lib/storage";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
|
||||
import { commit } from "./SetAuthTokenMutation";
|
||||
@@ -15,21 +17,29 @@ beforeAll(() => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Sets auth token to localStorage", () => {
|
||||
const context = {
|
||||
localStorage: createInMemoryStorage(),
|
||||
it("Sets auth token to localStorage", async () => {
|
||||
const clearSessionStub = sinon.stub();
|
||||
const context: Partial<TalkContext> = {
|
||||
localStorage: createPromisifiedStorage(),
|
||||
clearSession: clearSessionStub,
|
||||
};
|
||||
const authToken = "auth token";
|
||||
commit(environment, { authToken }, context as any);
|
||||
await commit(environment, { authToken }, context as any);
|
||||
expect(source.get(LOCAL_ID)!.authToken).toEqual(authToken);
|
||||
expect(context.localStorage.getItem("authToken")).toEqual(authToken);
|
||||
await expect(context.localStorage!.getItem("authToken")).resolves.toEqual(
|
||||
authToken
|
||||
);
|
||||
expect(clearSessionStub.calledOnce).toBe(true);
|
||||
});
|
||||
|
||||
it("Removes auth token from localStorage", () => {
|
||||
const context = {
|
||||
localStorage: createInMemoryStorage(),
|
||||
it("Removes auth token from localStorage", async () => {
|
||||
const clearSessionStub = sinon.stub();
|
||||
const context: Partial<TalkContext> = {
|
||||
localStorage: createPromisifiedStorage(),
|
||||
clearSession: clearSessionStub,
|
||||
};
|
||||
localStorage.setItem("authToken", "tmp");
|
||||
commit(environment, { authToken: null }, context as any);
|
||||
expect(context.localStorage.getItem("authToken")).toBeNull();
|
||||
await commit(environment, { authToken: null }, context as any);
|
||||
await expect(context.localStorage!.getItem("authToken")).resolves.toBeNull();
|
||||
expect(clearSessionStub.calledOnce).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import {
|
||||
commitLocalUpdatePromisified,
|
||||
createMutationContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
|
||||
|
||||
export interface SetAuthTokenInput {
|
||||
@@ -13,27 +16,18 @@ export type SetAuthTokenMutation = (input: SetAuthTokenInput) => Promise<void>;
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: SetAuthTokenInput,
|
||||
{ localStorage }: TalkContext
|
||||
{ localStorage, clearSession }: TalkContext
|
||||
) {
|
||||
return commitLocalUpdate(environment, store => {
|
||||
return await commitLocalUpdatePromisified(environment, async store => {
|
||||
const record = store.get(LOCAL_ID)!;
|
||||
record.setValue(input.authToken, "authToken");
|
||||
if (input.authToken) {
|
||||
localStorage.setItem("authToken", input.authToken);
|
||||
await localStorage.setItem("authToken", input.authToken);
|
||||
} else {
|
||||
localStorage.removeItem("authToken");
|
||||
await localStorage.removeItem("authToken");
|
||||
}
|
||||
// Increment auth revision to indicate a change in auth state.
|
||||
record.setValue(record.getValue("authRevision") + 1, "authRevision");
|
||||
|
||||
// Force gc to trigger.
|
||||
environment
|
||||
.retain({
|
||||
dataID: "tmp",
|
||||
node: { selections: [] },
|
||||
variables: {},
|
||||
})
|
||||
.dispose();
|
||||
// Clear current session, as we are starting a new one.
|
||||
clearSession();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,46 +3,40 @@ import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
TalkContext,
|
||||
TalkContextProvider,
|
||||
} from "talk-framework/lib/bootstrap";
|
||||
import { createManaged } from "talk-framework/lib/bootstrap";
|
||||
|
||||
import AppContainer from "./containers/AppContainer";
|
||||
import {
|
||||
onPostMessageAuthError,
|
||||
onPostMessageSetAuthToken,
|
||||
onPymSetCommentID,
|
||||
OnPostMessageAuthError,
|
||||
OnPostMessageSetAuthToken,
|
||||
OnPymSetCommentID,
|
||||
} from "./listeners";
|
||||
import { initLocalState } from "./local";
|
||||
import localesData from "./locales";
|
||||
|
||||
const listeners = [
|
||||
onPymSetCommentID,
|
||||
onPostMessageSetAuthToken,
|
||||
onPostMessageAuthError,
|
||||
];
|
||||
|
||||
// This is called when the context is first initialized.
|
||||
async function init(context: TalkContext) {
|
||||
await initLocalState(context.relayEnvironment, context);
|
||||
listeners.forEach(f => f(context));
|
||||
}
|
||||
const listeners = (
|
||||
<>
|
||||
<OnPymSetCommentID />
|
||||
<OnPostMessageSetAuthToken />
|
||||
<OnPostMessageAuthError />
|
||||
</>
|
||||
);
|
||||
|
||||
async function main() {
|
||||
// Bootstrap our context.
|
||||
const context = await createContext({
|
||||
init,
|
||||
const ManagedTalkContextProvider = await createManaged({
|
||||
initLocalState,
|
||||
localesData,
|
||||
userLocales: navigator.languages,
|
||||
pym: new PymChild({ polling: 100 }),
|
||||
});
|
||||
|
||||
const Index: StatelessComponent = () => (
|
||||
<TalkContextProvider value={context}>
|
||||
<AppContainer />
|
||||
</TalkContextProvider>
|
||||
<ManagedTalkContextProvider>
|
||||
<>
|
||||
{listeners}
|
||||
<AppContainer />
|
||||
</>
|
||||
</ManagedTalkContextProvider>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Index />, document.getElementById("app"));
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Component } from "react";
|
||||
|
||||
import { withContext } from "talk-framework/lib/bootstrap";
|
||||
import { PostMessageService } from "talk-framework/lib/postMessage";
|
||||
|
||||
interface Props {
|
||||
postMessage: PostMessageService;
|
||||
}
|
||||
|
||||
class OnPostMessageAuthError extends Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
// Auth popup will use this to send back errors during login.
|
||||
props.postMessage!.on("authError", error => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withContext(({ postMessage }) => ({ postMessage }))(
|
||||
OnPostMessageAuthError
|
||||
);
|
||||
export default enhanced;
|
||||
+11
-6
@@ -1,10 +1,14 @@
|
||||
import { shallow } from "enzyme";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { createInMemoryStorage } from "talk-framework/lib/storage";
|
||||
import { createPromisifiedStorage } from "talk-framework/lib/storage";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
|
||||
import onPostMessageSetAuthToken from "./onPostMessageSetAuthToken";
|
||||
import { OnPostMessageSetAuthToken } from "./OnPostMessageSetAuthToken";
|
||||
|
||||
let relayEnvironment: Environment;
|
||||
const source: RecordSource = new RecordSource();
|
||||
@@ -17,16 +21,17 @@ beforeAll(() => {
|
||||
|
||||
it("Sets auth token", () => {
|
||||
const token = "auth-token";
|
||||
const context = {
|
||||
const context: Partial<TalkContext> = {
|
||||
postMessage: {
|
||||
on: (name: string, cb: (token: string) => void) => {
|
||||
expect(name).toBe("setAuthToken");
|
||||
cb(token);
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
relayEnvironment,
|
||||
localStorage: createInMemoryStorage(),
|
||||
localStorage: createPromisifiedStorage(),
|
||||
clearSession: noop,
|
||||
};
|
||||
onPostMessageSetAuthToken(context as any);
|
||||
shallow(<OnPostMessageSetAuthToken context={context as any} />);
|
||||
expect(source.get(LOCAL_ID)!.authToken).toEqual(token);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Component } from "react";
|
||||
|
||||
import { TalkContext, withContext } from "talk-framework/lib/bootstrap";
|
||||
import { commit as setAuthToken } from "talk-framework/mutations/SetAuthTokenMutation";
|
||||
|
||||
interface Props {
|
||||
context: TalkContext;
|
||||
}
|
||||
|
||||
export class OnPostMessageSetAuthToken extends Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
// Auth popup will use this to handle a successful login.
|
||||
props.context.postMessage!.on("setAuthToken", (authToken: string) => {
|
||||
setAuthToken(
|
||||
this.props.context.relayEnvironment,
|
||||
{ authToken },
|
||||
this.props.context
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withContext(context => ({ context }))(
|
||||
OnPostMessageSetAuthToken
|
||||
);
|
||||
export default enhanced;
|
||||
+9
-7
@@ -1,9 +1,11 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
|
||||
import onPymSetCommentID from "./onPymSetCommentID";
|
||||
import { OnPymSetCommentID } from "./OnPymSetCommentID";
|
||||
|
||||
let relayEnvironment: Environment;
|
||||
const source: RecordSource = new RecordSource();
|
||||
@@ -16,30 +18,30 @@ beforeAll(() => {
|
||||
|
||||
it("Sets comment id", () => {
|
||||
const id = "comment1-id";
|
||||
const context = {
|
||||
const props = {
|
||||
pym: {
|
||||
onMessage: (eventName: string, cb: (id: string) => void) => {
|
||||
expect(eventName).toBe("setCommentID");
|
||||
cb(id);
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
relayEnvironment,
|
||||
};
|
||||
onPymSetCommentID(context as any);
|
||||
shallow(<OnPymSetCommentID {...props} />);
|
||||
expect(source.get(LOCAL_ID)!.commentID).toEqual(id);
|
||||
});
|
||||
|
||||
it("Sets comment id to null when empty", () => {
|
||||
const id = "";
|
||||
const context = {
|
||||
const props = {
|
||||
pym: {
|
||||
onMessage: (eventName: string, cb: (data: string) => void) => {
|
||||
expect(eventName).toBe("setCommentID");
|
||||
cb(id);
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
relayEnvironment,
|
||||
};
|
||||
onPymSetCommentID(context as any);
|
||||
shallow(<OnPymSetCommentID {...props} />);
|
||||
expect(source.get(LOCAL_ID)!.commentID).toEqual(null);
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Child } from "pym.js";
|
||||
import { Component } from "react";
|
||||
import { commitLocalUpdate } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { withContext } from "talk-framework/lib/bootstrap";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
|
||||
interface Props {
|
||||
relayEnvironment: Environment;
|
||||
pym: Child;
|
||||
}
|
||||
|
||||
export class OnPymSetCommentID extends Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Sets comment id through pym.
|
||||
props.pym!.onMessage("setCommentID", raw => {
|
||||
commitLocalUpdate(this.props.relayEnvironment, s => {
|
||||
const id = raw || null;
|
||||
if (s.get(LOCAL_ID)!.getValue("commentID") !== id) {
|
||||
s.get(LOCAL_ID)!.setValue(id, "commentID");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withContext(({ relayEnvironment, pym }) => ({
|
||||
relayEnvironment,
|
||||
pym,
|
||||
}))(OnPymSetCommentID);
|
||||
|
||||
export default enhanced;
|
||||
@@ -1,5 +1,5 @@
|
||||
export { default as onPymSetCommentID } from "./onPymSetCommentID";
|
||||
export { default as OnPymSetCommentID } from "./OnPymSetCommentID";
|
||||
export {
|
||||
default as onPostMessageSetAuthToken,
|
||||
} from "./onPostMessageSetAuthToken";
|
||||
export { default as onPostMessageAuthError } from "./onPostMessageAuthError";
|
||||
default as OnPostMessageSetAuthToken,
|
||||
} from "./OnPostMessageSetAuthToken";
|
||||
export { default as OnPostMessageAuthError } from "./OnPostMessageAuthError";
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
|
||||
export default function onPostMessageSetAuthToken({
|
||||
postMessage,
|
||||
}: TalkContext) {
|
||||
// Auth popup will use this to send back errors during login.
|
||||
postMessage!.on("authError", error => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
|
||||
import { commit as setAuthToken } from "talk-framework/mutations/SetAuthTokenMutation";
|
||||
|
||||
export default function onPostMessageSetAuthToken(ctx: TalkContext) {
|
||||
const { relayEnvironment, postMessage } = ctx;
|
||||
// Auth popup will use this to handle a successful login.
|
||||
postMessage!.on("setAuthToken", (authToken: string) => {
|
||||
setAuthToken(relayEnvironment, { authToken }, ctx);
|
||||
});
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { commitLocalUpdate } from "react-relay";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
|
||||
export default function onPymSetCommentID({
|
||||
relayEnvironment,
|
||||
pym,
|
||||
}: TalkContext) {
|
||||
// Sets comment id through pym.
|
||||
pym!.onMessage("setCommentID", raw => {
|
||||
commitLocalUpdate(relayEnvironment, s => {
|
||||
const id = raw || null;
|
||||
if (s.get(LOCAL_ID)!.getValue("commentID") !== id) {
|
||||
s.get(LOCAL_ID)!.setValue(id, "commentID");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -13,7 +13,6 @@ exports[`init local state 1`] = `
|
||||
\\"__id\\": \\"client:root.local\\",
|
||||
\\"__typename\\": \\"Local\\",
|
||||
\\"authToken\\": \\"\\",
|
||||
\\"authRevision\\": 0,
|
||||
\\"network\\": {
|
||||
\\"__ref\\": \\"client:root.local.network\\"
|
||||
},
|
||||
|
||||
@@ -35,9 +35,6 @@ export default async function initLocalState(
|
||||
// Set auth token
|
||||
localRecord.setValue(authToken || "", "authToken");
|
||||
|
||||
// Set initial auth revision, this is increment whenenver auth state might have changed.
|
||||
localRecord.setValue(0, "authRevision");
|
||||
|
||||
// Parse query params
|
||||
const query = qs.parse(location.search);
|
||||
|
||||
|
||||
@@ -26,10 +26,6 @@ type Local {
|
||||
commentID: String
|
||||
authPopup: AuthPopup!
|
||||
authToken: String
|
||||
# Used to invalidate the `me` endpoint.
|
||||
# This is incremented whenever the auth status
|
||||
# might have changed.
|
||||
authRevision: Int!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
|
||||
@@ -45,19 +45,12 @@ export const render = ({
|
||||
};
|
||||
|
||||
const PermalinkViewQuery: StatelessComponent<InnerProps> = ({
|
||||
local: { commentID, assetID, authRevision },
|
||||
local: { commentID, assetID },
|
||||
}) => (
|
||||
<QueryRenderer<QueryTypes>
|
||||
query={graphql`
|
||||
query PermalinkViewQuery(
|
||||
$commentID: ID!
|
||||
$assetID: ID!
|
||||
$authRevision: Int!
|
||||
) {
|
||||
# authRevision is increment every time auth state has changed.
|
||||
# This is basically a cache invalidation and causes relay
|
||||
# to automatically update this query.
|
||||
me(clientAuthRevision: $authRevision) {
|
||||
query PermalinkViewQuery($commentID: ID!, $assetID: ID!) {
|
||||
me {
|
||||
...PermalinkViewContainer_me
|
||||
}
|
||||
asset(id: $assetID) {
|
||||
@@ -71,7 +64,6 @@ const PermalinkViewQuery: StatelessComponent<InnerProps> = ({
|
||||
variables={{
|
||||
assetID: assetID!,
|
||||
commentID: commentID!,
|
||||
authRevision,
|
||||
}}
|
||||
render={render}
|
||||
/>
|
||||
@@ -81,7 +73,6 @@ const enhanced = withLocalStateContainer(
|
||||
graphql`
|
||||
fragment PermalinkViewQueryLocal on Local {
|
||||
assetID
|
||||
authRevision
|
||||
commentID
|
||||
}
|
||||
`
|
||||
|
||||
@@ -38,15 +38,12 @@ export const render = ({
|
||||
};
|
||||
|
||||
const StreamQuery: StatelessComponent<InnerProps> = ({
|
||||
local: { assetID, authRevision },
|
||||
local: { assetID },
|
||||
}) => (
|
||||
<QueryRenderer<QueryTypes>
|
||||
query={graphql`
|
||||
query StreamQuery($assetID: ID!, $authRevision: Int!) {
|
||||
# authRevision is increment every time auth state has changed.
|
||||
# This is basically a cache invalidation and causes relay
|
||||
# to automatically update this query.
|
||||
me(clientAuthRevision: $authRevision) {
|
||||
query StreamQuery($assetID: ID!) {
|
||||
me {
|
||||
...StreamContainer_me
|
||||
}
|
||||
asset(id: $assetID) {
|
||||
@@ -56,7 +53,6 @@ const StreamQuery: StatelessComponent<InnerProps> = ({
|
||||
`}
|
||||
variables={{
|
||||
assetID: assetID!,
|
||||
authRevision,
|
||||
}}
|
||||
render={render}
|
||||
/>
|
||||
@@ -66,7 +62,6 @@ const enhanced = withLocalStateContainer(
|
||||
graphql`
|
||||
fragment StreamQueryLocal on Local {
|
||||
assetID
|
||||
authRevision
|
||||
}
|
||||
`
|
||||
)(StreamQuery);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { IResolvers } from "graphql-tools";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
import TestRenderer from "react-test-renderer";
|
||||
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
|
||||
@@ -30,7 +32,6 @@ export default function create(params: CreateParams) {
|
||||
logNetwork: params.logNetwork,
|
||||
resolvers: params.resolvers,
|
||||
initLocalState: (localRecord, source, env) => {
|
||||
localRecord.setValue(0, "authRevision");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, env);
|
||||
}
|
||||
@@ -46,6 +47,8 @@ export default function create(params: CreateParams) {
|
||||
postMessage: new PostMessageService(),
|
||||
browserInfo: { ios: false },
|
||||
uuidGenerator: createUUIDGenerator(),
|
||||
eventEmitter: new EventEmitter2({ wildcard: true, maxListeners: 20 }),
|
||||
clearSession: noop,
|
||||
};
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
|
||||
@@ -11,14 +11,8 @@ let testRenderer: ReactTestRenderer;
|
||||
beforeEach(() => {
|
||||
const resolvers = {
|
||||
Query: {
|
||||
asset: createSinonStub(
|
||||
s => s.throws(),
|
||||
s => s.withArgs(undefined, { id: assets[0].id }).returns(assets[0])
|
||||
),
|
||||
me: createSinonStub(
|
||||
s => s.throws(),
|
||||
s => s.withArgs(undefined, { clientAuthRevision: 0 }).returns(users[0])
|
||||
),
|
||||
asset: createSinonStub(s => s.throws(), s => s.returns(assets[0])),
|
||||
me: createSinonStub(s => s.throws(), s => s.returns(users[0])),
|
||||
},
|
||||
Mutation: {
|
||||
createComment: createSinonStub(
|
||||
|
||||
@@ -11,14 +11,8 @@ let testRenderer: ReactTestRenderer;
|
||||
beforeEach(() => {
|
||||
const resolvers = {
|
||||
Query: {
|
||||
asset: createSinonStub(
|
||||
s => s.throws(),
|
||||
s => s.withArgs(undefined, { id: assets[0].id }).returns(assets[0])
|
||||
),
|
||||
me: createSinonStub(
|
||||
s => s.throws(),
|
||||
s => s.withArgs(undefined, { clientAuthRevision: 0 }).returns(users[0])
|
||||
),
|
||||
asset: createSinonStub(s => s.throws(), s => s.returns(assets[0])),
|
||||
me: createSinonStub(s => s.throws(), s => s.returns(users[0])),
|
||||
},
|
||||
Mutation: {
|
||||
createComment: createSinonStub(
|
||||
|
||||
@@ -809,12 +809,8 @@ type Query {
|
||||
|
||||
"""
|
||||
me is the current logged in User.
|
||||
|
||||
clientAuthRevision is an implementation detail that is only
|
||||
used on the client to invalidate the cache.
|
||||
TODO: This should move to a client side directive if this becomes possible.
|
||||
"""
|
||||
me(clientAuthRevision: Int): User
|
||||
me: User
|
||||
|
||||
"""
|
||||
settings is the Settings for a given Tenant.
|
||||
|
||||
Reference in New Issue
Block a user