mirror of
https://github.com/wassname/talk.git
synced 2026-09-09 11:38:08 +08:00
[next] Cookie Support (#2339)
* feat: added cookie support to coral * feat: adapt client to use cookies * fix: safari input styles * fix: lint * fix: linting * fix: support clearing cookies properly, oauth * feat: support cookies for websocket upgrade requests * fix: lint * fix: tests
This commit is contained in:
@@ -2,7 +2,6 @@ type Local {
|
||||
accessToken: String
|
||||
accessTokenExp: Int
|
||||
accessTokenJTI: String
|
||||
loggedIn: Boolean!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
|
||||
@@ -28,7 +28,6 @@ type Local {
|
||||
accessToken: String
|
||||
accessTokenExp: Int
|
||||
accessTokenJTI: String
|
||||
loggedIn: Boolean!
|
||||
redirectPath: String
|
||||
authView: View
|
||||
authError: String
|
||||
|
||||
@@ -12,13 +12,7 @@ const SignInMutation = createMutation(
|
||||
context: CoralContext
|
||||
) => {
|
||||
const result = await signIn(context.rest, input);
|
||||
|
||||
// Put the token on the hash and clean the session.
|
||||
// It'll be picked up by initLocalState.
|
||||
location.hash = `accessToken=${result.token}`;
|
||||
await context.clearSession();
|
||||
// TODO: (cvle) A better way would be if `context.clearSession` would return the new session and
|
||||
// we set the accessToken directly in there.
|
||||
await context.clearSession(result.token);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -764,4 +764,4 @@ exports[`shows server error 1`] = `
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`submits form successfully 1`] = `"http://localhost/admin/login#accessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIzMWIyNjU5MS00ZTlhLTQzODgtYTdmZi1lMWJkYzVkOTdjY2UifQ=="`;
|
||||
exports[`submits form successfully 1`] = `"http://localhost/admin/login"`;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { pureMerge } from "coral-common/utils";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
act,
|
||||
createAccessToken,
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
replaceHistoryLocation,
|
||||
@@ -40,8 +39,6 @@ async function createTestRenderer(
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue("SIGN_IN", "authView");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(createAccessToken(), "accessToken");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
createAccessToken,
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
replaceHistoryLocation,
|
||||
@@ -37,8 +36,6 @@ async function createTestRenderer(
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue("ADD_EMAIL_ADDRESS", "authView");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(createAccessToken(), "accessToken");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
createAccessToken,
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
replaceHistoryLocation,
|
||||
@@ -37,8 +36,6 @@ async function createTestRenderer(
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue("CREATE_PASSWORD", "authView");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(createAccessToken(), "accessToken");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
createAccessToken,
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
replaceHistoryLocation,
|
||||
@@ -31,8 +30,6 @@ async function createTestRenderer(
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue("CREATE_USERNAME", "authView");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(createAccessToken(), "accessToken");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
} from "coral-framework/schema";
|
||||
import {
|
||||
act,
|
||||
createAccessToken,
|
||||
createQueryResolverStub,
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
@@ -43,8 +42,6 @@ async function createTestRenderer(
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(createAccessToken(), "accessToken");
|
||||
localRecord.setValue("SIGN_IN", "authView");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
|
||||
@@ -39,7 +39,6 @@ async function createTestRenderer(
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(false, "loggedIn");
|
||||
localRecord.setValue("SIGN_IN", "authView");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { pureMerge } from "coral-common/utils";
|
||||
import { LOCAL_ID, lookup } from "coral-framework/lib/relay";
|
||||
import { GQLResolver, GQLUSER_ROLE } from "coral-framework/schema";
|
||||
import {
|
||||
createAccessToken,
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
replaceHistoryLocation,
|
||||
@@ -41,8 +40,6 @@ async function createTestRenderer(
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(createAccessToken(), "accessToken");
|
||||
localRecord.setValue("SIGN_IN", "authView");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
@@ -105,6 +102,5 @@ it("sign out when clicking on sign in as", async () => {
|
||||
"/admin/moderate/reported"
|
||||
);
|
||||
});
|
||||
|
||||
expect(lookup(context.relayEnvironment, LOCAL_ID)!.loggedIn).toBeFalsy();
|
||||
restMock.verify();
|
||||
});
|
||||
|
||||
@@ -41,7 +41,6 @@ async function createTestRenderer(
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(false, "loggedIn");
|
||||
localRecord.setValue("SIGN_IN", "authView");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import sinon from "sinon";
|
||||
import sinon, { SinonStub } from "sinon";
|
||||
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import { LOCAL_ID, lookup } from "coral-framework/lib/relay";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
createResolversStub,
|
||||
@@ -40,7 +39,6 @@ async function createTestRenderer(
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
@@ -77,6 +75,8 @@ it("logs out", async () => {
|
||||
signOutButton.props.onClick();
|
||||
|
||||
await wait(() => {
|
||||
expect(lookup(context.relayEnvironment, LOCAL_ID)!.loggedIn).toBeFalsy();
|
||||
restMock.verify();
|
||||
// Wait for new session to start.
|
||||
expect((context.clearSession as SinonStub).called).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,7 +52,6 @@ const createTestRenderer = async (
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ async function createTestRenderer(
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ async function createTestRenderer(
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ async function createTestRenderer(
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ async function createTestRenderer(
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ async function createTestRenderer(
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ async function createTestRenderer(
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ async function createTestRenderer(
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,6 @@ async function createTestRenderer(
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ async function createTestRenderer(
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`get auth token from url 1`] = `
|
||||
exports[`get access token from url 1`] = `
|
||||
"{
|
||||
\\"__id\\": \\"client:root.local\\",
|
||||
\\"__typename\\": \\"Local\\",
|
||||
\\"accessToken\\": \\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIzMWIyNjU5MS00ZTlhLTQzODgtYTdmZi1lMWJkYzVkOTdjY2UifQ==\\",
|
||||
\\"accessTokenJTI\\": \\"31b26591-4e9a-4388-a7ff-e1bdc5d97cce\\",
|
||||
\\"loggedIn\\": true,
|
||||
\\"view\\": \\"SIGN_IN\\",
|
||||
\\"error\\": null
|
||||
}"
|
||||
@@ -27,7 +26,6 @@ exports[`init local state 1`] = `
|
||||
\\"accessToken\\": \\"\\",
|
||||
\\"accessTokenExp\\": null,
|
||||
\\"accessTokenJTI\\": null,
|
||||
\\"loggedIn\\": false,
|
||||
\\"view\\": \\"SIGN_IN\\",
|
||||
\\"error\\": null
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ it("set view from query", async () => {
|
||||
restoreHistoryLocation();
|
||||
});
|
||||
|
||||
it("get auth token from url", async () => {
|
||||
it("get access token from url", async () => {
|
||||
const restoreHistoryLocation = replaceHistoryLocation(
|
||||
`http://localhost/#accessToken=${createAccessToken()}`
|
||||
);
|
||||
|
||||
@@ -11,7 +11,6 @@ type Local {
|
||||
accessToken: String
|
||||
accessTokenExp: Int
|
||||
accessTokenJTI: String
|
||||
loggedIn: Boolean!
|
||||
view: View!
|
||||
error: String
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ exports[`auth configuration renders all auth enabled 1`] = `
|
||||
>
|
||||
<a
|
||||
className="TextLink-root"
|
||||
href="http://localhost/?view=FORGOT_PASSWORD#accessToken=access-token"
|
||||
href="http://localhost/?view=FORGOT_PASSWORD"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Forgot your password?
|
||||
|
||||
@@ -122,7 +122,7 @@ it("do not render createPassword view when local auth is disabled", async () =>
|
||||
await wait(() => expect(windowMock.closeStub.called).toBe(true));
|
||||
});
|
||||
|
||||
it("send back auth token", async () => {
|
||||
it("send back access token", async () => {
|
||||
const { context } = await createTestRenderer({
|
||||
Query: {
|
||||
viewer: {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { get } from "lodash";
|
||||
import sinon from "sinon";
|
||||
import sinon, { SinonStub } from "sinon";
|
||||
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import {
|
||||
createAccessToken,
|
||||
toJSON,
|
||||
wait,
|
||||
waitForElement,
|
||||
@@ -165,7 +166,7 @@ it("shows server error", async () => {
|
||||
it("submits form successfully", async () => {
|
||||
const { form, context } = await createTestRenderer();
|
||||
const { getByLabelText } = within(form!);
|
||||
const accessToken = "access-token";
|
||||
const accessToken = createAccessToken();
|
||||
const emailAddressField = getByLabelText("Email Address");
|
||||
const passwordField = getByLabelText("Password");
|
||||
const submitButton = form!.find(
|
||||
@@ -198,8 +199,13 @@ it("submits form successfully", async () => {
|
||||
|
||||
expect(toJSON(form!)).toMatchSnapshot();
|
||||
|
||||
// Wait for window hash to contain a token.
|
||||
await wait(() => expect(location.hash).toBe(`#accessToken=${accessToken}`));
|
||||
// Wait for new session to start.
|
||||
await wait(() =>
|
||||
expect((context.clearSession as SinonStub).calledWith(accessToken)).toBe(
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
restMock.verify();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { get, merge } from "lodash";
|
||||
import sinon from "sinon";
|
||||
import sinon, { SinonStub } from "sinon";
|
||||
|
||||
import {
|
||||
createAccessToken,
|
||||
toJSON,
|
||||
wait,
|
||||
waitForElement,
|
||||
@@ -186,7 +187,7 @@ it("shows server error", async () => {
|
||||
it("submits form successfully", async () => {
|
||||
const { context, main, form } = await createTestRenderer();
|
||||
const { getByLabelText } = within(form!);
|
||||
const accessToken = "access-token";
|
||||
const accessToken = createAccessToken();
|
||||
const emailAddressField = getByLabelText("Email Address");
|
||||
const usernameField = getByLabelText("Username");
|
||||
const passwordField = getByLabelText("Password");
|
||||
@@ -223,8 +224,12 @@ it("submits form successfully", async () => {
|
||||
|
||||
expect(toJSON(main)).toMatchSnapshot();
|
||||
|
||||
// Wait for window hash to contain a token.
|
||||
await wait(() => expect(location.hash).toBe(`#accessToken=${accessToken}`));
|
||||
// Wait for new session to start.
|
||||
await wait(() =>
|
||||
expect((context.clearSession as SinonStub).calledWith(accessToken)).toBe(
|
||||
true
|
||||
)
|
||||
);
|
||||
restMock.verify();
|
||||
});
|
||||
|
||||
|
||||
@@ -13,12 +13,7 @@ export async function commit(
|
||||
{ rest, clearSession }: CoralContext
|
||||
) {
|
||||
const result = await signIn(rest, pick(input, ["email", "password"]));
|
||||
// Put the token on the hash and clean the session.
|
||||
// It'll be picked up by initLocalState.
|
||||
location.hash = `accessToken=${result.token}`;
|
||||
await clearSession();
|
||||
// TODO: (cvle) A better way would be if `context.clearSession` would return the new session and
|
||||
// we set the accessToken directly in there.
|
||||
await clearSession(result.token);
|
||||
}
|
||||
|
||||
export const withSignInMutation = createMutationContainer("signIn", commit);
|
||||
|
||||
@@ -16,10 +16,7 @@ export async function commit(
|
||||
rest,
|
||||
pick(input, ["email", "password", "username"])
|
||||
);
|
||||
// Put the token on the hash and clean the session.
|
||||
// It'll be picked up by initLocalState.
|
||||
location.hash = `accessToken=${result.token}`;
|
||||
await clearSession();
|
||||
await clearSession(result.token);
|
||||
}
|
||||
|
||||
export const withSignUpMutation = createMutationContainer("signUp", commit);
|
||||
|
||||
@@ -62,7 +62,7 @@ export interface CoralContext {
|
||||
eventEmitter: EventEmitter2;
|
||||
|
||||
/** Clear session data. */
|
||||
clearSession: () => Promise<void>;
|
||||
clearSession: (nextAccessToken?: string | null) => Promise<void>;
|
||||
|
||||
/** Controls router transitions (for tests) */
|
||||
transitionControl?: TransitionControlData;
|
||||
|
||||
@@ -8,7 +8,11 @@ import { Environment, RecordSource, Store } from "relay-runtime";
|
||||
import uuid from "uuid/v1";
|
||||
|
||||
import { getBrowserInfo } from "coral-framework/lib/browserInfo";
|
||||
import { LOCAL_ID } from "coral-framework/lib/relay";
|
||||
import {
|
||||
commitLocalUpdatePromisified,
|
||||
LOCAL_ID,
|
||||
setAccessTokenInLocalState,
|
||||
} from "coral-framework/lib/relay";
|
||||
import {
|
||||
createLocalStorage,
|
||||
createPromisifiedStorage,
|
||||
@@ -105,7 +109,7 @@ function createRelayEnvironment(
|
||||
const tokenGetter: TokenGetter = () => {
|
||||
const localState = source.get(LOCAL_ID);
|
||||
if (localState) {
|
||||
return (localState.loggedIn && localState.accessToken) || "";
|
||||
return localState.accessToken || "";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
@@ -145,7 +149,7 @@ function createMangedCoralContextProvider(
|
||||
}
|
||||
|
||||
// This is called every time a user session starts or ends.
|
||||
private clearSession = async () => {
|
||||
private clearSession = async (nextAccessToken?: string | null) => {
|
||||
// Clear session storage.
|
||||
this.state.context.sessionStorage.clear();
|
||||
|
||||
@@ -173,6 +177,13 @@ function createMangedCoralContextProvider(
|
||||
// a new token.
|
||||
subscriptionClient.setAccessToken(newTokenGetter());
|
||||
|
||||
// Set next access token.
|
||||
if (nextAccessToken) {
|
||||
await commitLocalUpdatePromisified(newEnvironment, async store => {
|
||||
setAccessTokenInLocalState(nextAccessToken, store);
|
||||
});
|
||||
}
|
||||
|
||||
// Propagate new context.
|
||||
this.setState(
|
||||
{
|
||||
@@ -292,7 +303,7 @@ export default async function createManaged({
|
||||
uuidGenerator: uuid,
|
||||
// Noop, this is later replaced by the
|
||||
// managed CoralContextProvider.
|
||||
clearSession: () => Promise.resolve(),
|
||||
clearSession: (nextAccessToken?: string | null) => Promise.resolve(),
|
||||
};
|
||||
|
||||
// Initialize local state.
|
||||
|
||||
@@ -25,14 +25,13 @@ export function setAccessTokenInLocalState(
|
||||
const localRecord = source.get(LOCAL_ID)!;
|
||||
localRecord.setValue(accessToken || "", "accessToken");
|
||||
if (accessToken) {
|
||||
const { payload, expired } = parseJWT(accessToken);
|
||||
const { payload } = parseJWT(accessToken);
|
||||
localRecord.setValue(payload.exp, "accessTokenExp");
|
||||
localRecord.setValue(payload.jti, "accessTokenJTI");
|
||||
localRecord.setValue(!expired, "loggedIn");
|
||||
// TODO: (cvle) maybe a timer to detect when accessToken has expired?
|
||||
} else {
|
||||
localRecord.setValue(null, "accessTokenExp");
|
||||
localRecord.setValue(null, "accessTokenJTI");
|
||||
localRecord.setValue(false, "loggedIn");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +40,6 @@ export async function initLocalBaseState(
|
||||
{ localStorage }: CoralContext,
|
||||
accessToken?: string | null
|
||||
) {
|
||||
if (accessToken === undefined) {
|
||||
accessToken = await localStorage!.getItem("accessToken");
|
||||
}
|
||||
|
||||
commitLocalUpdate(environment, s => {
|
||||
const root = s.getRoot();
|
||||
|
||||
@@ -52,7 +47,7 @@ export async function initLocalBaseState(
|
||||
const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE);
|
||||
root.setLinkedRecord(localRecord, "local");
|
||||
|
||||
// Set auth token
|
||||
// Set access token
|
||||
setAccessTokenInLocalState(accessToken || null, s);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import { LOCAL_ID } from "coral-framework/lib/relay";
|
||||
import { createPromisifiedStorage } from "coral-framework/lib/storage";
|
||||
import {
|
||||
createAccessToken,
|
||||
createRelayEnvironment,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import SetAccessTokenMutation from "./SetAccessTokenMutation";
|
||||
|
||||
let environment: Environment;
|
||||
const source: RecordSource = new RecordSource();
|
||||
|
||||
beforeAll(() => {
|
||||
environment = createRelayEnvironment({
|
||||
source,
|
||||
});
|
||||
});
|
||||
|
||||
const accessToken = createAccessToken();
|
||||
|
||||
it("Sets auth token to localStorage", async () => {
|
||||
const clearSessionStub = sinon.stub();
|
||||
const context: Partial<CoralContext> = {
|
||||
localStorage: createPromisifiedStorage(),
|
||||
clearSession: clearSessionStub,
|
||||
};
|
||||
await SetAccessTokenMutation.commit(
|
||||
environment,
|
||||
{ accessToken },
|
||||
context as any
|
||||
);
|
||||
expect(source.get(LOCAL_ID)!.accessToken).toEqual(accessToken);
|
||||
await expect(context.localStorage!.getItem("accessToken")).resolves.toEqual(
|
||||
accessToken
|
||||
);
|
||||
expect(clearSessionStub.calledOnce).toBe(true);
|
||||
});
|
||||
|
||||
it("Removes auth token from localStorage", async () => {
|
||||
const clearSessionStub = sinon.stub();
|
||||
const context: Partial<CoralContext> = {
|
||||
localStorage: createPromisifiedStorage(),
|
||||
clearSession: clearSessionStub,
|
||||
};
|
||||
localStorage.setItem("accessToken", accessToken);
|
||||
await SetAccessTokenMutation.commit(
|
||||
environment,
|
||||
{ accessToken: null },
|
||||
context as any
|
||||
);
|
||||
await expect(
|
||||
context.localStorage!.getItem("accessToken")
|
||||
).resolves.toBeNull();
|
||||
expect(clearSessionStub.calledOnce).toBe(true);
|
||||
});
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitLocalUpdatePromisified,
|
||||
createMutation,
|
||||
setAccessTokenInLocalState,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { createMutation } from "coral-framework/lib/relay";
|
||||
|
||||
interface SetAccessTokenInput {
|
||||
accessToken: string | null;
|
||||
@@ -16,18 +12,10 @@ const SetAccessTokenMutation = createMutation(
|
||||
async (
|
||||
environment: Environment,
|
||||
input: SetAccessTokenInput,
|
||||
{ localStorage, clearSession }: CoralContext
|
||||
{ clearSession }: CoralContext
|
||||
) => {
|
||||
await commitLocalUpdatePromisified(environment, async store => {
|
||||
setAccessTokenInLocalState(input.accessToken, store);
|
||||
if (input.accessToken) {
|
||||
await localStorage.setItem("accessToken", input.accessToken);
|
||||
} else {
|
||||
await localStorage.removeItem("accessToken");
|
||||
}
|
||||
// Clear current session, as we are starting a new one.
|
||||
await clearSession();
|
||||
});
|
||||
// Clear current session, as we are starting a new one.
|
||||
await clearSession(input.accessToken);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { RestClient } from "coral-framework/lib/rest";
|
||||
import { createPromisifiedStorage } from "coral-framework/lib/storage";
|
||||
import { createUUIDGenerator } from "coral-framework/testHelpers";
|
||||
|
||||
import sinon from "sinon";
|
||||
import createFluentBundle from "./createFluentBundle";
|
||||
import createRelayEnvironment from "./createRelayEnvironment";
|
||||
import createSubscriptionHandler, {
|
||||
@@ -102,13 +103,12 @@ export default function createTestRenderer<
|
||||
browserInfo: params.browserInfo || { ios: false },
|
||||
uuidGenerator: createUUIDGenerator(),
|
||||
eventEmitter: new EventEmitter2({ wildcard: true, maxListeners: 20 }),
|
||||
clearSession: () => Promise.resolve(),
|
||||
clearSession: sinon.stub(),
|
||||
transitionControl: {
|
||||
allowTransition: true,
|
||||
history: [],
|
||||
},
|
||||
};
|
||||
|
||||
let testRenderer: ReactTestRenderer;
|
||||
TestRenderer.act(() => {
|
||||
testRenderer = TestRenderer.create(
|
||||
|
||||
@@ -31,16 +31,16 @@ export class TabBarContainer extends Component<Props> {
|
||||
|
||||
public render() {
|
||||
const {
|
||||
local: { loggedIn, activeTab },
|
||||
local: { activeTab },
|
||||
viewer,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TabBar
|
||||
activeTab={activeTab}
|
||||
showProfileTab={loggedIn}
|
||||
showProfileTab={Boolean(viewer)}
|
||||
showConfigureTab={
|
||||
!!this.props.viewer &&
|
||||
can(this.props.viewer, Ability.CHANGE_STORY_CONFIGURATION)
|
||||
!!viewer && can(viewer, Ability.CHANGE_STORY_CONFIGURATION)
|
||||
}
|
||||
onTabClick={this.handleSetActiveTab}
|
||||
/>
|
||||
@@ -52,7 +52,6 @@ const enhanced = withSetActiveTabMutation(
|
||||
withLocalStateContainer(
|
||||
graphql`
|
||||
fragment TabBarContainerLocal on Local {
|
||||
loggedIn
|
||||
activeTab
|
||||
}
|
||||
`
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createSinonStub } from "coral-framework/testHelpers";
|
||||
|
||||
import { OnPostMessageSetAccessToken } from "./OnPostMessageSetAccessToken";
|
||||
|
||||
it("Listens to event and sets auth token", () => {
|
||||
it("Listens to event and sets access token", () => {
|
||||
const token = "access-token";
|
||||
const postMessage: any = {
|
||||
on: (name: string, cb: (token: string) => void) => {
|
||||
|
||||
@@ -17,6 +17,7 @@ it("renders fully", () => {
|
||||
focus: false,
|
||||
view: "SIGN_IN",
|
||||
},
|
||||
accessToken: "access-token",
|
||||
accessTokenJTI: "JTI",
|
||||
},
|
||||
viewer: null,
|
||||
@@ -74,6 +75,7 @@ it("renders without logout button", () => {
|
||||
focus: false,
|
||||
view: "SIGN_IN",
|
||||
},
|
||||
accessToken: "access-token",
|
||||
accessTokenJTI: null,
|
||||
},
|
||||
viewer: null,
|
||||
@@ -131,6 +133,7 @@ it("renders sso only", () => {
|
||||
focus: false,
|
||||
view: "SIGN_IN",
|
||||
},
|
||||
accessToken: "access-token",
|
||||
accessTokenJTI: "JTI",
|
||||
},
|
||||
viewer: null,
|
||||
@@ -188,6 +191,7 @@ it("renders sso only without logout button", () => {
|
||||
focus: false,
|
||||
view: "SIGN_IN",
|
||||
},
|
||||
accessToken: "access-token",
|
||||
accessTokenJTI: "JTI",
|
||||
},
|
||||
viewer: null,
|
||||
@@ -245,6 +249,7 @@ it("renders without register button", () => {
|
||||
focus: false,
|
||||
view: "SIGN_IN",
|
||||
},
|
||||
accessToken: "access-token",
|
||||
accessTokenJTI: "JTI",
|
||||
},
|
||||
viewer: null,
|
||||
|
||||
@@ -45,7 +45,9 @@ export class UserBoxContainer extends Component<Props> {
|
||||
private handleSignOut = () => this.props.signOut();
|
||||
|
||||
private get supportsLogout() {
|
||||
return !!this.props.local.accessTokenJTI;
|
||||
return Boolean(
|
||||
!this.props.local.accessToken || this.props.local.accessTokenJTI
|
||||
);
|
||||
}
|
||||
|
||||
private get supportsRegister() {
|
||||
@@ -125,6 +127,7 @@ const enhanced = withSignOutMutation(
|
||||
focus
|
||||
view
|
||||
}
|
||||
accessToken
|
||||
accessTokenJTI
|
||||
}
|
||||
`
|
||||
|
||||
@@ -15,7 +15,6 @@ exports[`init local state 1`] = `
|
||||
\\"accessToken\\": \\"\\",
|
||||
\\"accessTokenExp\\": null,
|
||||
\\"accessTokenJTI\\": null,
|
||||
\\"loggedIn\\": false,
|
||||
\\"commentsOrderBy\\": \\"CREATED_AT_DESC\\",
|
||||
\\"authPopup\\": {
|
||||
\\"__ref\\": \\"client:root.local.authPopup\\"
|
||||
|
||||
@@ -5,7 +5,6 @@ import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import { LOCAL_ID } from "coral-framework/lib/relay";
|
||||
import { createPromisifiedStorage } from "coral-framework/lib/storage";
|
||||
import {
|
||||
createAccessToken,
|
||||
createRelayEnvironment,
|
||||
replaceHistoryLocation,
|
||||
} from "coral-framework/testHelpers";
|
||||
@@ -57,13 +56,3 @@ it("set commentID from query", async () => {
|
||||
expect(source.get(LOCAL_ID)!.commentID).toBe(commentID);
|
||||
restoreHistoryLocation();
|
||||
});
|
||||
|
||||
it("set accessToken from localStorage", async () => {
|
||||
const context: Partial<CoralContext> = {
|
||||
localStorage: createPromisifiedStorage(),
|
||||
};
|
||||
const accessToken = createAccessToken();
|
||||
context.localStorage!.setItem("accessToken", accessToken);
|
||||
await initLocalState(environment, context as any);
|
||||
expect(source.get(LOCAL_ID)!.accessToken).toBe(accessToken);
|
||||
});
|
||||
|
||||
@@ -46,7 +46,6 @@ type Local {
|
||||
accessToken: String
|
||||
accessTokenExp: Int
|
||||
accessTokenJTI: String
|
||||
loggedIn: Boolean!
|
||||
activeTab: TAB!
|
||||
authPopup: AuthPopup!
|
||||
storyID: String
|
||||
|
||||
@@ -23,7 +23,7 @@ class ReportButtonContainer extends React.Component<
|
||||
ReportButtonContainerProps
|
||||
> {
|
||||
private get loggedIn() {
|
||||
return this.props.viewer;
|
||||
return Boolean(this.props.viewer);
|
||||
}
|
||||
private handleSignIn = () => this.props.showAuthPopup({ view: "SIGN_IN" });
|
||||
|
||||
|
||||
+3
-3
@@ -19,9 +19,6 @@ type Props = PropTypesOf<typeof PostCommentFormContainerN>;
|
||||
function createDefaultProps(add: DeepPartial<Props> = {}): Props {
|
||||
return pureMerge(
|
||||
{
|
||||
local: {
|
||||
loggedIn: true,
|
||||
},
|
||||
showAuthPopup: noop as any,
|
||||
createComment: noop as any,
|
||||
refreshSettings: noop as any,
|
||||
@@ -49,6 +46,9 @@ function createDefaultProps(add: DeepPartial<Props> = {}): Props {
|
||||
message: "",
|
||||
},
|
||||
},
|
||||
viewer: {
|
||||
id: "viewer-id",
|
||||
},
|
||||
},
|
||||
add
|
||||
);
|
||||
|
||||
+39
-43
@@ -11,13 +11,12 @@ import {
|
||||
graphql,
|
||||
withFetch,
|
||||
withFragmentContainer,
|
||||
withLocalStateContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { PromisifiedStorage } from "coral-framework/lib/storage";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import { PostCommentFormContainer_settings as SettingsData } from "coral-stream/__generated__/PostCommentFormContainer_settings.graphql";
|
||||
import { PostCommentFormContainer_story as StoryData } from "coral-stream/__generated__/PostCommentFormContainer_story.graphql";
|
||||
import { PostCommentFormContainerLocal as Local } from "coral-stream/__generated__/PostCommentFormContainerLocal.graphql";
|
||||
import { PostCommentFormContainer_settings } from "coral-stream/__generated__/PostCommentFormContainer_settings.graphql";
|
||||
import { PostCommentFormContainer_story } from "coral-stream/__generated__/PostCommentFormContainer_story.graphql";
|
||||
import { PostCommentFormContainer_viewer } from "coral-stream/__generated__/PostCommentFormContainer_viewer.graphql";
|
||||
import {
|
||||
ShowAuthPopupMutation,
|
||||
withShowAuthPopupMutation,
|
||||
@@ -42,9 +41,9 @@ interface Props {
|
||||
createComment: CreateCommentMutation;
|
||||
refreshSettings: FetchProp<typeof RefreshSettingsFetch>;
|
||||
sessionStorage: PromisifiedStorage;
|
||||
settings: SettingsData;
|
||||
local: Local;
|
||||
story: StoryData;
|
||||
settings: PostCommentFormContainer_settings;
|
||||
viewer: PostCommentFormContainer_viewer | null;
|
||||
story: PostCommentFormContainer_story;
|
||||
showAuthPopup: ShowAuthPopupMutation;
|
||||
}
|
||||
|
||||
@@ -71,7 +70,7 @@ export class PostCommentFormContainer extends Component<Props, State> {
|
||||
initialized: false,
|
||||
nudge: true,
|
||||
keepFormWhenClosed:
|
||||
this.props.local.loggedIn &&
|
||||
!!this.props.viewer &&
|
||||
!this.props.story.isClosed &&
|
||||
!this.props.settings.disableCommenting.enabled,
|
||||
submitStatus: null,
|
||||
@@ -194,7 +193,7 @@ export class PostCommentFormContainer extends Component<Props, State> {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!this.props.local.loggedIn) {
|
||||
if (!this.props.viewer) {
|
||||
return (
|
||||
<PostCommentFormFake
|
||||
draft={this.state.notLoggedInDraft}
|
||||
@@ -243,44 +242,41 @@ const enhanced = withContext(({ sessionStorage }) => ({
|
||||
withShowAuthPopupMutation(
|
||||
withCreateCommentMutation(
|
||||
withFetch(RefreshSettingsFetch)(
|
||||
withLocalStateContainer(
|
||||
graphql`
|
||||
fragment PostCommentFormContainerLocal on Local {
|
||||
loggedIn
|
||||
withFragmentContainer<Props>({
|
||||
settings: graphql`
|
||||
fragment PostCommentFormContainer_settings on Settings {
|
||||
charCount {
|
||||
enabled
|
||||
min
|
||||
max
|
||||
}
|
||||
disableCommenting {
|
||||
enabled
|
||||
message
|
||||
}
|
||||
closeCommenting {
|
||||
message
|
||||
}
|
||||
}
|
||||
`
|
||||
)(
|
||||
withFragmentContainer<Props>({
|
||||
settings: graphql`
|
||||
fragment PostCommentFormContainer_settings on Settings {
|
||||
charCount {
|
||||
`,
|
||||
story: graphql`
|
||||
fragment PostCommentFormContainer_story on Story {
|
||||
id
|
||||
isClosed
|
||||
...MessageBoxContainer_story
|
||||
settings {
|
||||
messageBox {
|
||||
enabled
|
||||
min
|
||||
max
|
||||
}
|
||||
disableCommenting {
|
||||
enabled
|
||||
message
|
||||
}
|
||||
closeCommenting {
|
||||
message
|
||||
}
|
||||
}
|
||||
`,
|
||||
story: graphql`
|
||||
fragment PostCommentFormContainer_story on Story {
|
||||
id
|
||||
isClosed
|
||||
...MessageBoxContainer_story
|
||||
settings {
|
||||
messageBox {
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(PostCommentFormContainer)
|
||||
)
|
||||
}
|
||||
`,
|
||||
viewer: graphql`
|
||||
fragment PostCommentFormContainer_viewer on User {
|
||||
id
|
||||
}
|
||||
`,
|
||||
})(PostCommentFormContainer)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -85,6 +85,7 @@ export const StreamContainer: FunctionComponent<Props> = props => {
|
||||
<PostCommentFormContainer
|
||||
settings={props.settings}
|
||||
story={props.story}
|
||||
viewer={props.viewer}
|
||||
/>
|
||||
)}
|
||||
{banned && <BannedInfo />}
|
||||
@@ -170,6 +171,7 @@ const enhanced = withFragmentContainer<Props>({
|
||||
...UserBoxContainer_viewer
|
||||
...CreateCommentReplyMutation_viewer
|
||||
...CreateCommentMutation_viewer
|
||||
...PostCommentFormContainer_viewer
|
||||
status {
|
||||
current
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ export default function create(params: CreateTestRendererParams) {
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue("COMMENTS", "activeTab");
|
||||
localRecord.setValue("ALL_COMMENTS", "commentsTab");
|
||||
localRecord.setValue(false, "loggedIn");
|
||||
localRecord.setValue("jti", "accessTokenJTI");
|
||||
localRecord.setValue("CREATED_AT_DESC", "commentsOrderBy");
|
||||
if (params.initLocalState) {
|
||||
|
||||
@@ -60,7 +60,6 @@ const createTestRenderer = async (
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(storyFixture.id, "storyID");
|
||||
localRecord.setValue(commentFixture.id, "commentID");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ async function createTestRenderer(
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(story.id, "storyID");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ async function createTestRenderer(
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(stories[0].id, "storyID");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ async function createTestRenderer(
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(stories[0].id, "storyID");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ async function createTestRenderer(
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(stories[0].id, "storyID");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ function createTestRenderer(
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(storyWithReplies.id, "storyID");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
},
|
||||
});
|
||||
return testRenderer;
|
||||
|
||||
@@ -38,7 +38,6 @@ async function createTestRenderer(
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(story.id, "storyID");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ async function createTestRenderer(
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(story.id, "storyID");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ async function createTestRenderer(
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(story.id, "storyID");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ async function createTestRenderer(
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(stories[0].id, "storyID");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@ beforeEach(() => {
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(storyWithDeepestReplies.id, "storyID");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -41,7 +41,6 @@ async function createTestRenderer(
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(stories[0].id, "storyID");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ function createTestRenderer() {
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(stories[0].id, "storyID");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
},
|
||||
});
|
||||
return { testRenderer, resolvers };
|
||||
|
||||
@@ -32,7 +32,6 @@ async function createTestRenderer(
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(storyWithNoComments.id, "storyID");
|
||||
localRecord.setValue(Boolean(data.loggedIn), "loggedIn");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ function createTestRenderer(
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(stories[0].id, "storyID");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
},
|
||||
});
|
||||
return { testRenderer, resolvers };
|
||||
|
||||
@@ -11,7 +11,6 @@ export default function create(params: CreateTestRendererParams) {
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue("PROFILE", "activeTab");
|
||||
localRecord.setValue("jti", "accessTokenJTI");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(story.id, "storyID");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--round-corners);
|
||||
width: 100%;
|
||||
line-height: 36px;
|
||||
align-self: stretch;
|
||||
|
||||
&:read-only {
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
font-weight: var(--font-weight-regular);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
font-size: calc(16rem / var(--rem-base));
|
||||
line-height: calc(18em / 16);
|
||||
line-height: 1;
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
color: var(--palette-text-primary);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user