[next] Auth Popup v2 (#2101)

* feat: Implement new Sign In view

* feat: Move forgot + resetPassword to new design

* feat: Implement sign up with new design

* fix: narrow gutter

* test: add unit tests

* test: integration tests

* feat: support show / hide password

* feat: support oauth2 flow

* feat: add views for user completion

* feat: implement oauth2 sign up

* test: fix snapshots

* fix: lint

* fix: get more complete mutation response

* fix: removed array of OIDC integrations

* fix: renamed resolver function

* fix: adapt oidc client implementation

* fix: targetFilter should be stream on signup

* fix: removed unneeded message

* fix: moved password into local profile

* fix: made username optional, removed valid null value

* fix: linting

* fix: respect targetFilter

* feat: support user registration mutations

- Added `setUsername`
- Added `setEmail`
- Added `setPassword`
- Added `permit` to `@auth`
- Added `email` to `User`

* fix: fixed issue with query

* feat: added user password update

* feat: complete sign in mutation

* fix: adapt some rebasing gitches

* test: improve tests

* test: unittest for setting auth token

* fix: failing tests

* test: move most tests from enzyme to react-test-renderer

* fix: remove schema warnings in tests

* test: improve window mock

* test: test different social login configurations

* test: test social logins for sign up

* fix: use htmlFor instead of for

* test: more feature tests

* feat: always go through account completion

* test: feature test account completion

* feat: addtional account completion test

* Update start.ts

* chore: refactor auth token retrieval logic
This commit is contained in:
Kiwi
2018-12-20 22:32:04 +01:00
committed by GitHub
parent 326a10dc5d
commit 065cb4b03a
331 changed files with 12476 additions and 7163 deletions
@@ -13,7 +13,7 @@ interface State {
copied: boolean;
}
class PermalinkPopover extends React.Component<InnerProps> {
class CopyButton extends React.Component<InnerProps> {
private timeout: any = null;
public state: State = {
@@ -59,4 +59,4 @@ class PermalinkPopover extends React.Component<InnerProps> {
}
}
export default PermalinkPopover;
export default CopyButton;
@@ -1 +0,0 @@
export { default } from "./CopyButton";
@@ -0,0 +1,22 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Omit, PropTypesOf } from "talk-framework/types";
import { PasswordField as PasswordFieldUI } from "talk-ui/components";
export interface Props
extends Omit<
PropTypesOf<typeof PasswordFieldUI>,
"showPasswordTitle" | "hidePasswordTitle"
> {}
const PasswordField: StatelessComponent<Props> = props => (
<Localized
id="framework-passwordField"
attrs={{ showPasswordTitle: true, hidePasswordTitle: true }}
>
<PasswordFieldUI {...props} />
</Localized>
);
export default PasswordField;
@@ -1 +1,2 @@
export { default as CopyButton } from "./CopyButton";
export { default as PasswordField } from "./PasswordField";
@@ -54,6 +54,12 @@ export const PASSWORDS_DO_NOT_MATCH = () => (
</Localized>
);
export const EMAILS_DO_NOT_MATCH = () => (
<Localized id="framework-validation-emailsDoNotMatch">
<span>Emails do not match. Try again.</span>
</Localized>
);
export const INVALID_URL = () => (
<Localized id="framework-validation-invalidURL">
<span>Invalid URL</span>
@@ -6,19 +6,21 @@ import { FetchFunction } from "relay-runtime";
*/
export default function wrapFetchWithLogger(
fetch: FetchFunction,
logResult?: boolean
options: { logResult?: boolean; muteErrors?: boolean } = {}
): FetchFunction {
return async (...args: any[]) => {
try {
const result = await (fetch as any)(...args);
if (logResult) {
if (options.logResult) {
// tslint:disable-next-line:no-console
console.log(JSON.stringify(result));
}
return result;
} catch (err) {
// tslint:disable-next-line:no-console
console.error(err);
if (!options.muteErrors) {
// tslint:disable-next-line:no-console
console.error(err);
}
throw err;
}
};
+11 -2
View File
@@ -1,5 +1,6 @@
import { ReactNode } from "react";
import {
EMAILS_DO_NOT_MATCH,
INVALID_CHARACTERS,
INVALID_EMAIL,
INVALID_URL,
@@ -101,10 +102,18 @@ export const validatePassword = createValidator(
PASSWORD_TOO_SHORT(8)
);
/**s
* validateUsername is a Validator that checks that the value is a valid username.
/**
* validateEqualPasswords is a Validator that checks for correct password confirmation.
*/
export const validateEqualPasswords = createValidator(
(v, values) => v === values.password,
PASSWORDS_DO_NOT_MATCH()
);
/**
* validateEqualEmails is a Validator that checks for correct email confirmation.
*/
export const validateEqualEmails = createValidator(
(v, values) => v === values.email,
EMAILS_DO_NOT_MATCH()
);
@@ -4,7 +4,10 @@ import sinon from "sinon";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import {
createAuthToken,
createRelayEnvironment,
} from "talk-framework/testHelpers";
import { commit } from "./SetAuthTokenMutation";
@@ -17,17 +20,7 @@ beforeAll(() => {
});
});
const authToken = `${btoa(
JSON.stringify({
alg: "HS256",
typ: "JWT",
})
)}.${btoa(
JSON.stringify({
exp: 1540503165,
jti: "31b26591-4e9a-4388-a7ff-e1bdc5d97cce",
})
)}`;
const authToken = createAuthToken();
it("Sets auth token to localStorage", async () => {
const clearSessionStub = sinon.stub();
@@ -1,10 +1,12 @@
import { ReactTestInstance } from "react-test-renderer";
import { queryAllByText } from "./byText";
import matchText, { TextMatchOptions, TextMatchPattern } from "./matchText";
const matcher = (pattern: TextMatchPattern, options?: TextMatchOptions) => (
i: ReactTestInstance
) => {
const ariaLabelMatcher = (
pattern: TextMatchPattern,
options?: TextMatchOptions
) => (i: ReactTestInstance) => {
// Only look at dom components.
if (typeof i.type !== "string" || !i.props["aria-label"]) {
return false;
@@ -20,31 +22,14 @@ export function getByLabelText(
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
return container.find(matcher(pattern, options));
}
export function queryByLabelText(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
try {
return container.find(matcher(pattern, options));
} catch {
return null;
const results = queryAllByLabelText(container, pattern, options);
if (results.length === 1) {
return results[0];
}
}
export function queryAllByLabelText(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
try {
return container.findAll(matcher(pattern, options));
} catch {
return [];
if (results.length === 0) {
throw new Error(`Could't find element with label text ${pattern}`);
}
throw new Error(`Found multiple elements with label text ${pattern}`);
}
export function getAllByLabelText(
@@ -52,5 +37,55 @@ export function getAllByLabelText(
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
return container.findAll(matcher(pattern, options));
const results = queryAllByLabelText(container, pattern, options);
if (results.length) {
return results;
}
throw new Error(`Could't find element with label text ${pattern}`);
}
export function queryByLabelText(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
const results = queryAllByLabelText(container, pattern, options);
if (results.length) {
return results[0];
}
return null;
}
export function queryAllByLabelText(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
const matches = container.findAll(ariaLabelMatcher(pattern, options));
queryAllByText(container, pattern).forEach(i => {
if (typeof i.type !== "string") {
return;
}
if (i.props.id) {
try {
matches.push(
container.find(
x =>
typeof x.type === "string" &&
x.props["aria-labelledby"] === i.props.id
)
);
} catch {} // tslint:disable-line:no-empty
}
if (i.type === "label" && i.props.htmlFor) {
try {
matches.push(
container.find(
x => typeof x.type === "string" && x.props.id === i.props.htmlFor
)
);
} catch {} // tslint:disable-line:no-empty
}
});
return matches;
}
@@ -6,10 +6,10 @@ const matcher = (pattern: TextMatchPattern, options?: TextMatchOptions) => (
i: ReactTestInstance
) => {
// Only look at dom components.
if (typeof i.type !== "string" || !i.props["data-test"]) {
if (typeof i.type !== "string" || !i.props["data-testid"]) {
return false;
}
return matchText(pattern, i.props["data-test"], {
return matchText(pattern, i.props["data-testid"], {
collapseWhitespace: false,
...options,
});
@@ -23,34 +23,34 @@ export function getByTestID(
return container.find(matcher(pattern, options));
}
export function getAllByTestID(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
const results = container.findAll(matcher(pattern, options));
if (!results.length) {
throw new Error(`Couldn't find test id ${pattern}`);
}
return results;
}
export function queryByTestID(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
try {
return container.find(matcher(pattern, options));
} catch {
const results = container.findAll(matcher(pattern, options));
if (!results.length) {
return null;
}
return results[0];
}
export function queryAllByTestID(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
try {
return container.findAll(matcher(pattern, options));
} catch {
return [];
}
}
export function getAllByTestID(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
return container.findAll(matcher(pattern, options));
}
+15 -15
View File
@@ -30,34 +30,34 @@ export function getByText(
return container.find(matcher(pattern, options));
}
export function getAllByText(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
const results = container.findAll(matcher(pattern, options));
if (!results.length) {
throw new Error(`Couldn't find text ${pattern}`);
}
return results;
}
export function queryByText(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
try {
return container.find(matcher(pattern, options));
} catch {
const results = container.findAll(matcher(pattern, options));
if (!results.length) {
return null;
}
return results[0];
}
export function queryAllByText(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
try {
return container.findAll(matcher(pattern, options));
} catch {
return [];
}
}
export function getAllByText(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
return container.findAll(matcher(pattern, options));
}
@@ -0,0 +1,56 @@
import { ReactTestInstance } from "react-test-renderer";
import matchText, { TextMatchOptions, TextMatchPattern } from "./matchText";
const matcher = (pattern: TextMatchPattern, options?: TextMatchOptions) => (
i: ReactTestInstance
) => {
// Only look at dom components.
if (typeof i.type !== "string") {
return false;
}
return matchText(pattern, i.type, {
collapseWhitespace: false,
...options,
});
};
export function getByType(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
return container.find(matcher(pattern, options));
}
export function getAllByType(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
const results = container.findAll(matcher(pattern, options));
if (!results.length) {
throw new Error(`Couldn't find test id ${pattern}`);
}
return results;
}
export function queryByType(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
const results = container.findAll(matcher(pattern, options));
if (!results.length) {
return null;
}
return results[0];
}
export function queryAllByType(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
return container.findAll(matcher(pattern, options));
}
@@ -0,0 +1,12 @@
export default function createAuthToken() {
return `${btoa(
JSON.stringify({
alg: "HS256",
typ: "JWT",
})
)}.${btoa(
JSON.stringify({
jti: "31b26591-4e9a-4388-a7ff-e1bdc5d97cce",
})
)}`;
}
@@ -26,6 +26,8 @@ export interface CreateRelayEnvironmentNetworkParams {
resolvers: IResolvers<any, any>;
/** If enabled, graphql responses will be logged to the console */
logNetwork?: boolean;
/** If enabled, graphql errors will be muted */
muteNetworkErrors?: boolean;
}
export interface CreateRelayEnvironmentParams {
@@ -58,10 +60,14 @@ export default function createRelayEnvironment(
if (params.network) {
const schema = loadSchema(
params.network.projectName,
params.network.resolvers
params.network.resolvers,
{ requireResolversForResolveType: false }
);
network = Network.create(
wrapFetchWithLogger(createFetch({ schema }), params.network.logNetwork)
wrapFetchWithLogger(createFetch({ schema }), {
logResult: params.network.logNetwork,
muteErrors: params.network.muteNetworkErrors,
})
);
}
const environment = new Environment({
+2 -14
View File
@@ -10,25 +10,13 @@ export {
} from "./removeFragmentRefs";
export { default as createUUIDGenerator } from "./createUUIDGenerator";
export * from "./denormalize";
export { default as replaceHistoryLocation } from "./replaceHistoryLocation";
export { default as limitSnapshotTo } from "./limitSnapshotTo";
export { default as inputPredicate } from "./inputPredicate";
export {
getByTestID,
getAllByTestID,
queryByTestID,
queryAllByTestID,
} from "./byTestID";
export { getByText, getAllByText, queryByText, queryAllByText } from "./byText";
export {
getByLabelText,
getAllByLabelText,
queryByLabelText,
queryAllByLabelText,
} from "./byLabelText";
export { default as within } from "./within";
export { default as wait } from "./wait";
export { default as waitForElement } from "./waitForElement";
export { default as waitUntilThrow } from "./waitUntilThrow";
export { default as matchText } from "./matchText";
export { default as toJSON } from "./toJSON";
export { default as replaceHistoryLocation } from "./replaceHistoryLocation";
export { default as createAuthToken } from "./createAuthToken";
@@ -1,5 +1,5 @@
export default function limitSnapshotTo(dataTest: string, node: any) {
if (node.props && node.props["data-test"] === dataTest) {
if (node.props && node.props["data-testid"] === dataTest) {
return node;
}
if (node.children) {
@@ -13,6 +13,12 @@ import {
queryByTestID,
} from "./byTestID";
import { getAllByText, getByText, queryAllByText, queryByText } from "./byText";
import {
getAllByType,
getByType,
queryAllByType,
queryByType,
} from "./byType";
import toJSON from "./toJSON";
type Func0<R> = () => R;
@@ -49,6 +55,10 @@ export default function within(container: ReactTestInstance) {
getAllByLabelText: applyContainer(container, getAllByLabelText),
queryByLabelText: applyContainer(container, queryByLabelText),
queryAllByLabelText: applyContainer(container, queryAllByLabelText),
getByType: applyContainer(container, getByType),
getAllByType: applyContainer(container, getAllByType),
queryByType: applyContainer(container, queryByType),
queryAllByType: applyContainer(container, queryAllByType),
toJSON: () => toJSON(container),
}
}