[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
View File
@@ -1,18 +1,5 @@
/* Here we add global stylings for body and document */
:global {
body {
margin: 0;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
}
}
.root {
width: 100%;
padding: var(--spacing-unit) var(--spacing-unit) calc(2 * var(--spacing-unit))
var(--spacing-unit);
box-sizing: border-box;
}
+9 -4
View File
@@ -1,14 +1,19 @@
import { shallow } from "enzyme";
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import { PropTypesOf } from "talk-framework/types";
import App from "./App";
const AppN = removeFragmentRefs(App);
it("renders sign in", () => {
const props: PropTypesOf<typeof App> = {
const props: PropTypesOf<typeof AppN> = {
view: "SIGN_IN",
auth: {},
};
const wrapper = shallow(<App {...props} />);
expect(wrapper).toMatchSnapshot();
const renderer = createRenderer();
renderer.render(<AppN {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
+25 -10
View File
@@ -1,40 +1,55 @@
import React, { StatelessComponent } from "react";
import ForgotPasswordContainer from "../containers/ForgotPasswordContainer";
import ResetPasswordContainer from "../containers/ResetPasswordContainer";
import SignInContainer from "../containers/SignInContainer";
import SignUpContainer from "../containers/SignUpContainer";
import { PropTypesOf } from "talk-framework/types";
import styles from "./App.css";
import AddEmailAddressContainer from "../views/addEmailAddress/containers/AddEmailAddressContainer";
import CreatePasswordContainer from "../views/createPassword/containers/CreatePasswordContainer";
import CreateUsernameContainer from "../views/createUsername/containers/CreateUsernameContainer";
import ForgotPasswordContainer from "../views/forgotPassword/containers/ForgotPasswordContainer";
import ResetPasswordContainer from "../views/resetPassword/containers/ResetPasswordContainer";
import SignInContainer from "../views/signIn/containers/SignInContainer";
import SignUpContainer from "../views/signUp/containers/SignUpContainer";
import "./App.css";
export type View =
| "SIGN_UP"
| "SIGN_IN"
| "FORGOT_PASSWORD"
| "RESET_PASSWORD"
| "CREATE_USERNAME"
| "CREATE_PASSWORD"
| "ADD_EMAIL_ADDRESS"
| "%future added value";
export interface AppProps {
view: View;
auth: PropTypesOf<typeof SignInContainer>["auth"] &
PropTypesOf<typeof SignUpContainer>["auth"];
}
const renderView = (view: View) => {
const renderView = (view: AppProps["view"], auth: AppProps["auth"]) => {
switch (view) {
case "SIGN_UP":
return <SignUpContainer />;
return <SignUpContainer auth={auth} />;
case "SIGN_IN":
return <SignInContainer />;
return <SignInContainer auth={auth} />;
case "FORGOT_PASSWORD":
return <ForgotPasswordContainer />;
case "RESET_PASSWORD":
return <ResetPasswordContainer />;
case "CREATE_USERNAME":
return <CreateUsernameContainer />;
case "CREATE_PASSWORD":
return <CreatePasswordContainer />;
case "ADD_EMAIL_ADDRESS":
return <AddEmailAddressContainer />;
default:
throw new Error(`Unknown view ${view}`);
}
};
const App: StatelessComponent<AppProps> = ({ view }) => (
<div className={styles.root}>{renderView(view)}</div>
const App: StatelessComponent<AppProps> = ({ view, auth }) => (
<div>{renderView(view, auth)}</div>
);
export default App;
@@ -0,0 +1,61 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { Field } from "react-final-form";
import {
composeValidators,
required,
validateEqualEmails,
} from "talk-framework/lib/validation";
import {
FormField,
InputLabel,
TextField,
ValidationMessage,
} from "talk-ui/components";
interface Props {
disabled: boolean;
}
const ConfirmEmailField: StatelessComponent<Props> = props => (
<Field
name="confirmEmail"
validate={composeValidators(required, validateEqualEmails)}
>
{({ input, meta }) => (
<FormField>
<Localized id="general-confirmEmailAddressLabel">
<InputLabel htmlFor={input.name}>Confirm Email Address</InputLabel>
</Localized>
<Localized
id="general-confirmEmailAddressTextField"
attrs={{ placeholder: true }}
>
<TextField
id={input.name}
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Confirm Email Address"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={props.disabled}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
);
export default ConfirmEmailField;
@@ -0,0 +1,63 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { Field } from "react-final-form";
import {
composeValidators,
required,
validateEqualPasswords,
} from "talk-framework/lib/validation";
import {
FormField,
InputLabel,
TextField,
ValidationMessage,
} from "talk-ui/components";
interface Props {
disabled: boolean;
}
const SetPasswordField: StatelessComponent<Props> = props => (
<Field
name="confirmPassword"
validate={composeValidators(required, validateEqualPasswords)}
>
{({ input, meta }) => (
<FormField>
<Localized id="general-confirmPasswordLabel">
<InputLabel htmlFor={input.name}>Confirm Password</InputLabel>
</Localized>
<Localized
id="general-confirmPasswordTextField"
attrs={{ placeholder: true }}
>
<TextField
id={input.name}
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Confirm Password"
type="password"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={props.disabled}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
);
export default SetPasswordField;
@@ -0,0 +1,58 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { Field } from "react-final-form";
import {
composeValidators,
required,
validateEmail,
} from "talk-framework/lib/validation";
import {
FormField,
InputLabel,
TextField,
ValidationMessage,
} from "talk-ui/components";
interface Props {
disabled: boolean;
}
const EmailField: StatelessComponent<Props> = props => (
<Field name="email" validate={composeValidators(required, validateEmail)}>
{({ input, meta }) => (
<FormField>
<Localized id="general-emailAddressLabel">
<InputLabel htmlFor={input.name}>Email Address</InputLabel>
</Localized>
<Localized
id="general-emailAddressTextField"
attrs={{ placeholder: true }}
>
<TextField
id={input.name}
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Email Address"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={props.disabled}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
);
export default EmailField;
@@ -0,0 +1,16 @@
.variantFilled {
&.colorRegular {
background-color: #3b5998;
}
&:not(.disabled) {
&.colorRegular {
&.mouseHover {
background-color: #4467b0;
}
&:active,
&.active {
background-color: #6583c3;
}
}
}
}
@@ -0,0 +1,17 @@
import { noop } from "lodash";
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { PropTypesOf } from "talk-framework/types";
import FacebookButton from "./FacebookButton";
it("renders correctly", () => {
const props: PropTypesOf<typeof FacebookButton> = {
onClick: noop,
children: "Login with Facebook",
};
const renderer = createRenderer();
renderer.render(<FacebookButton {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,42 @@
import React from "react";
import { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { Button } from "talk-ui/components";
import styles from "./FacebookButton.css";
interface Props {
onClick: PropTypesOf<typeof Button>["onClick"];
children: React.ReactNode;
}
const facebookIcon = (
<svg
width="17"
height="17"
viewBox="0 0 17 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.0893 1.25391C16.0893 1.00781 15.9838 0.796875 15.808 0.621094C15.6323 0.480469 15.4213 0.375 15.2104 0.375H1.21819C0.936942 0.375 0.726005 0.480469 0.58538 0.621094C0.409598 0.796875 0.339286 1.00781 0.339286 1.25391V15.2461C0.339286 15.4922 0.409598 15.7031 0.58538 15.8789C0.726005 16.0547 0.936942 16.125 1.21819 16.125H8.74163V10.0078H6.70257V7.65234H8.74163V5.89453C8.74163 4.91016 9.02288 4.13672 9.58538 3.57422C10.1479 3.04688 10.8862 2.76562 11.8002 2.76562C12.5033 2.76562 13.1362 2.80078 13.6283 2.83594V4.98047H12.3627C11.9057 4.98047 11.5893 5.08594 11.4135 5.29688C11.2729 5.47266 11.2026 5.75391 11.2026 6.14062V7.65234H13.558L13.2416 10.0078H11.2026V16.125H15.2104C15.4565 16.125 15.6674 16.0547 15.8432 15.8789C15.9838 15.7031 16.0893 15.4922 16.0893 15.2461V1.25391Z"
fill="white"
/>
</svg>
);
const FacebookButton: StatelessComponent<Props> = props => (
<Button
classes={styles}
variant="filled"
size="large"
fullWidth
onClick={props.onClick}
>
{facebookIcon}
<span>{props.children}</span>
</Button>
);
export default FacebookButton;
@@ -1,110 +0,0 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field, Form } from "react-final-form";
import { OnSubmit } from "talk-framework/lib/form";
import {
composeValidators,
required,
validateEmail,
} from "talk-framework/lib/validation";
import {
Button,
CallOut,
FormField,
HorizontalGutter,
InputLabel,
TextField,
Typography,
ValidationMessage,
} from "talk-ui/components";
import AutoHeightContainer from "../containers/AutoHeightContainer";
interface FormProps {
email: string;
}
export interface ForgotPasswordForm {
onSubmit: OnSubmit<FormProps>;
}
const ForgotPassword: StatelessComponent<ForgotPasswordForm> = props => {
return (
<Form onSubmit={props.onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<AutoHeightContainer />
<HorizontalGutter size="double">
<Localized id="forgotPassword-forgotPasswordHeader">
<Typography variant="heading1" align="center">
Forgot Password
</Typography>
</Localized>
<Localized id="forgotPassword-enterEmailAndGetALink">
<Typography variant="bodyCopy">
Enter your email address below and we will send you a link to
reset your password.
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<Field
name="email"
validate={composeValidators(required, validateEmail)}
>
{({ input, meta }) => (
<FormField>
<Localized id="forgotPassword-emailAddressLabel">
<InputLabel>Email Address</InputLabel>
</Localized>
<Localized
id="forgotPassword-emailAddressTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Email Address"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
fullWidth
disabled={submitting}
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Localized id="forgotPassword-sendEmailButton">
<Button
variant="filled"
color="primary"
size="large"
fullWidth
disabled={submitting}
>
Send Email
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
);
};
export default ForgotPassword;
@@ -0,0 +1,16 @@
.variantFilled {
&.colorRegular {
background-color: #db4437;
}
&:not(.disabled) {
&.colorRegular {
&.mouseHover {
background-color: #e05f54;
}
&:active,
&.active {
background-color: #e57a71;
}
}
}
}
@@ -0,0 +1,17 @@
import { noop } from "lodash";
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { PropTypesOf } from "talk-framework/types";
import GoogleButton from "./GoogleButton";
it("renders correctly", () => {
const props: PropTypesOf<typeof GoogleButton> = {
onClick: noop,
children: "Login with Google",
};
const renderer = createRenderer();
renderer.render(<GoogleButton {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,42 @@
import React from "react";
import { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { Button } from "talk-ui/components";
import styles from "./GoogleButton.css";
interface Props {
onClick: PropTypesOf<typeof Button>["onClick"];
children: React.ReactNode;
}
const googleIcon = (
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.2924 9.46094C17.2924 9.00391 17.2221 8.51172 17.1518 7.98438H8.85491V11.0078H13.8119C13.7065 11.5352 13.4955 12.0625 13.2143 12.5547C12.7924 13.1875 12.2651 13.7148 11.6323 14.0664C10.8237 14.5586 9.9096 14.7695 8.85491 14.7695C7.87054 14.7695 6.95647 14.5234 6.14788 14.0312C5.30413 13.5391 4.67132 12.8711 4.17913 12.0273C3.68694 11.1836 3.44085 10.2695 3.44085 9.25C3.44085 8.08984 3.75725 7.03516 4.39007 6.12109C4.95257 5.27734 5.726 4.64453 6.71038 4.22266C7.6596 3.80078 8.64397 3.66016 9.62835 3.80078C10.683 3.94141 11.5619 4.39844 12.3354 5.10156L14.6908 2.81641C13.0737 1.30469 11.1049 0.53125 8.85491 0.53125C7.27288 0.53125 5.83147 0.953125 4.49554 1.72656C3.1596 2.5 2.06975 3.55469 1.29632 4.89062C0.52288 6.22656 0.136161 7.70312 0.136161 9.25C0.136161 10.832 0.52288 12.2734 1.29632 13.6094C2.06975 14.9453 3.1596 16.0352 4.49554 16.8086C5.83147 17.582 7.27288 17.9688 8.85491 17.9688C10.5073 17.9688 11.9838 17.6172 13.2494 16.8789C14.5151 16.1758 15.5346 15.1562 16.2377 13.8555C16.9408 12.5898 17.2924 11.1133 17.2924 9.46094Z"
fill="white"
/>
</svg>
);
const GoogleButton: StatelessComponent<Props> = props => (
<Button
classes={styles}
variant="filled"
size="large"
fullWidth
onClick={props.onClick}
>
{googleIcon}
<span>{props.children}</span>
</Button>
);
export default GoogleButton;
@@ -0,0 +1,4 @@
.root {
min-height: calc(var(--spacing-unit) * 8);
background-color: var(--palette-text-primary);
}
@@ -0,0 +1,15 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { PropTypesOf } from "talk-framework/types";
import Bar from "./Bar";
it("renders correctly", () => {
const props: PropTypesOf<typeof Bar> = {
children: "Hello World",
};
const renderer = createRenderer();
renderer.render(<Bar {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,18 @@
import React from "react";
import { StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import styles from "./Bar.css";
export interface BarProps {
children: React.ReactNode;
}
const Bar: StatelessComponent<BarProps> = props => (
<Flex className={styles.root} alignItems="center" justifyContent="center">
<div>{props.children}</div>
</Flex>
);
export default Bar;
@@ -0,0 +1,4 @@
.root {
min-height: calc(var(--spacing-unit) * 4);
border-bottom: 1px solid var(--palette-divider);
}
@@ -0,0 +1,15 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { PropTypesOf } from "talk-framework/types";
import SubBar from "./SubBar";
it("renders correctly", () => {
const props: PropTypesOf<typeof SubBar> = {
children: "Hello World",
};
const renderer = createRenderer();
renderer.render(<SubBar {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,18 @@
import React from "react";
import { StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import styles from "./SubBar.css";
export interface SubBarProps {
children: React.ReactNode;
}
const SubBar: StatelessComponent<SubBarProps> = props => (
<Flex className={styles.root} alignItems="center" justifyContent="center">
<div>{props.children}</div>
</Flex>
);
export default SubBar;
@@ -0,0 +1,3 @@
.root {
color: var(--palette-text-light);
}
@@ -0,0 +1,15 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { PropTypesOf } from "talk-framework/types";
import Subtitle from "./Subtitle";
it("renders correctly", () => {
const props: PropTypesOf<typeof Subtitle> = {
children: "Hello World",
};
const renderer = createRenderer();
renderer.render(<Subtitle {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,17 @@
import React from "react";
import { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
import styles from "./Subtitle.css";
interface Props {
children?: React.ReactNode;
}
const Subtitle: StatelessComponent<Props> = props => (
<Typography variant="heading4" align="center" className={styles.root}>
{props.children}
</Typography>
);
export default Subtitle;
@@ -0,0 +1,3 @@
.root {
color: var(--palette-text-light);
}
@@ -0,0 +1,15 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { PropTypesOf } from "talk-framework/types";
import Title from "./Title";
it("renders correctly", () => {
const props: PropTypesOf<typeof Title> = {
children: "Hello World",
};
const renderer = createRenderer();
renderer.render(<Title {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,17 @@
import React from "react";
import { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
import styles from "./Title.css";
interface Props {
children?: React.ReactNode;
}
const Title: StatelessComponent<Props> = props => (
<Typography variant="heading2" align="center" className={styles.root}>
{props.children}
</Typography>
);
export default Title;
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Flex)
alignItems="center"
className="Bar-root"
justifyContent="center"
>
<div>
Hello World
</div>
</withPropsOnChange(Flex)>
`;
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Flex)
alignItems="center"
className="SubBar-root"
justifyContent="center"
>
<div>
Hello World
</div>
</withPropsOnChange(Flex)>
`;
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Typography)
align="center"
className="Subtitle-root"
variant="heading4"
>
Hello World
</withPropsOnChange(Typography)>
`;
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Typography)
align="center"
className="Title-root"
variant="heading2"
>
Hello World
</withPropsOnChange(Typography)>
`;
@@ -0,0 +1,4 @@
export { default as Bar } from "./Bar";
export { default as SubBar } from "./SubBar";
export { default as Title } from "./Title";
export { default as Subtitle } from "./Subtitle";
+5
View File
@@ -0,0 +1,5 @@
.root {
width: 100%;
padding: calc(2.5 * var(--spacing-unit)) calc(1.5 * var(--spacing-unit));
box-sizing: border-box;
}
@@ -0,0 +1,15 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { PropTypesOf } from "talk-framework/types";
import Main from "./Main";
it("renders correctly", () => {
const props: PropTypesOf<typeof Main> = {
children: "Hello World",
};
const renderer = createRenderer();
renderer.render(<Main {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
+21
View File
@@ -0,0 +1,21 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import styles from "./Main.css";
export interface MainProps {
className?: string;
children: React.ReactNode;
}
const Main: StatelessComponent<MainProps> = ({
children,
className,
...rest
}) => (
<div {...rest} className={cn(styles.root, className)}>
{children}
</div>
);
export default Main;
@@ -0,0 +1,16 @@
.variantFilled {
&.colorRegular {
background-color: #0d5b8f;
}
&:not(.disabled) {
&.colorRegular {
&.mouseHover {
background-color: #106fae;
}
&:active,
&.active {
background-color: #1383cd;
}
}
}
}
@@ -0,0 +1,17 @@
import { noop } from "lodash";
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { PropTypesOf } from "talk-framework/types";
import OIDCButton from "./OIDCButton";
it("renders correctly", () => {
const props: PropTypesOf<typeof OIDCButton> = {
onClick: noop,
children: "Login with OIDC",
};
const renderer = createRenderer();
renderer.render(<OIDCButton {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,26 @@
import React from "react";
import { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { Button } from "talk-ui/components";
import styles from "./OIDCButton.css";
interface Props {
onClick: PropTypesOf<typeof Button>["onClick"];
children: React.ReactNode;
}
const OIDCButton: StatelessComponent<Props> = props => (
<Button
classes={styles}
variant="filled"
size="large"
fullWidth
onClick={props.onClick}
>
<span>{props.children}</span>
</Button>
);
export default OIDCButton;
@@ -0,0 +1,16 @@
.root {
position: relative;
}
.hr {
position: absolute;
border: 0;
border-top: 1px solid var(--palette-divider);
width: 100%;
margin: 0;
}
.text {
composes: heading3 from "talk-ui/shared/typography.css";
position: relative;
background-color: var(--palette-common-white);
padding: 0 var(--spacing-unit);
}
@@ -0,0 +1,10 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import OrSeparator from "./OrSeparator";
it("renders correctly", () => {
const renderer = createRenderer();
renderer.render(<OrSeparator />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,19 @@
import { Localized } from "fluent-react/compat";
import React from "react";
import { StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import styles from "./OrSeparator.css";
const OrSeparator: StatelessComponent = props => (
<Flex className={styles.root} alignItems="center" justifyContent="center">
<hr className={styles.hr} />
<Localized id="general-orSeparator">
<div className={styles.text}>Or</div>
</Localized>
</Flex>
);
export default OrSeparator;
@@ -1,154 +0,0 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field, Form } from "react-final-form";
import { OnSubmit } from "talk-framework/lib/form";
import {
composeValidators,
required,
validateEqualPasswords,
validatePassword,
} from "talk-framework/lib/validation";
import {
Button,
CallOut,
FormField,
HorizontalGutter,
InputDescription,
InputLabel,
TextField,
Typography,
ValidationMessage,
} from "talk-ui/components";
import AutoHeightContainer from "../containers/AutoHeightContainer";
interface FormProps {
password: string;
confirmPassword: string;
}
export interface ResetPasswordForm {
onSubmit: OnSubmit<FormProps>;
}
const ResetPassword: StatelessComponent<ResetPasswordForm> = props => {
return (
<Form onSubmit={props.onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<AutoHeightContainer />
<HorizontalGutter size="double">
<Localized id="resetPassword-resetPasswordHeader">
<Typography variant="heading1" align="center">
Reset Password
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<Field
name="password"
validate={composeValidators(required, validatePassword)}
>
{({ input, meta }) => (
<FormField>
<Localized id="resetPassword-passwordLabel">
<InputLabel>Password</InputLabel>
</Localized>
<Localized
id="resetPassword-passwordDescription"
$minLength={8}
>
<InputDescription>
{"Must be at least {$minLength} characters"}
</InputDescription>
</Localized>
<Localized
id="resetPassword-passwordTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Password"
type="password"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Field
name="confirmPassword"
validate={composeValidators(required, validateEqualPasswords)}
>
{({ input, meta }) => (
<FormField>
<Localized id="resetPassword-confirmPasswordLabel">
<InputLabel>Confirm Password</InputLabel>
</Localized>
<Localized
id="resetPassword-confirmPasswordTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Confirm Password"
type="password"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Localized id="resetPassword-resetPasswordButton">
<Button
variant="filled"
color="primary"
size="large"
fullWidth
disabled={submitting}
>
Reset Password
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
);
};
export default ResetPassword;
@@ -0,0 +1,64 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { PasswordField } from "talk-framework/components";
import {
composeValidators,
required,
validatePassword,
} from "talk-framework/lib/validation";
import {
FormField,
InputDescription,
InputLabel,
ValidationMessage,
} from "talk-ui/components";
interface Props {
disabled: boolean;
}
const SetPasswordField: StatelessComponent<Props> = props => (
<Field
name="password"
validate={composeValidators(required, validatePassword)}
>
{({ input, meta }) => (
<FormField>
<Localized id="general-passwordLabel">
<InputLabel htmlFor={input.name}>Password</InputLabel>
</Localized>
<Localized id="general-passwordDescription" $minLength={8}>
<InputDescription>
{"Must be at least {$minLength} characters"}
</InputDescription>
</Localized>
<Localized id="general-passwordTextField" attrs={{ placeholder: true }}>
<PasswordField
id={input.name}
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Password"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={props.disabled}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
);
export default SetPasswordField;
-179
View File
@@ -1,179 +0,0 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field, Form } from "react-final-form";
import { OnSubmit } from "talk-framework/lib/form";
import {
composeValidators,
required,
validateEmail,
} from "talk-framework/lib/validation";
import {
Button,
CallOut,
Flex,
FormField,
HorizontalGutter,
InputLabel,
TextField,
Typography,
ValidationMessage,
} from "talk-ui/components";
import AutoHeightContainer from "../containers/AutoHeightContainer";
interface FormProps {
email: string;
password: string;
}
export interface SignInForm {
onSubmit: OnSubmit<FormProps>;
onGotoSignUp: () => void;
onGotoForgotPassword: () => void;
}
const SignIn: StatelessComponent<SignInForm> = props => {
return (
<Form onSubmit={props.onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<AutoHeightContainer />
<HorizontalGutter size="double">
<Localized id="signIn-signInToJoinHeader">
<Typography variant="heading1" align="center">
Sign in to join the conversation
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<Field
name="email"
validate={composeValidators(required, validateEmail)}
>
{({ input, meta }) => (
<FormField>
<Localized id="signIn-emailAddressLabel">
<InputLabel>Email Address</InputLabel>
</Localized>
<Localized
id="signIn-emailAddressTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Email Address"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Field name="password" validate={composeValidators(required)}>
{({ input, meta }) => (
<FormField>
<Localized id="signIn-passwordLabel">
<InputLabel>Password</InputLabel>
</Localized>
<Localized
id="signIn-passwordTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Password"
type="password"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
<Flex justifyContent="flex-end">
<Localized id="signIn-forgotYourPassword">
<Button
id="signIn-gotoForgotPasswordButton"
variant="underlined"
color="primary"
size="small"
disabled={submitting}
onClick={props.onGotoForgotPassword}
>
Forgot your password?
</Button>
</Localized>
</Flex>
</FormField>
)}
</Field>
<Localized id="signIn-signInAndJoinButton">
<Button
variant="filled"
color="primary"
size="large"
type="submit"
disabled={submitting}
fullWidth
>
Sign in and join the conversation
</Button>
</Localized>
<Flex justifyContent="center">
<Localized
id="signIn-noAccountSignUp"
button={
<Button
id="signIn-gotoSignUpButton"
variant="underlined"
size="small"
color="primary"
disabled={submitting}
onClick={props.onGotoSignUp}
/>
}
>
<Typography variant="bodyCopy" container={Flex}>
{"Don't have an account? <button>Sign Up</button>"}
</Typography>
</Localized>
</Flex>
</HorizontalGutter>
</form>
)}
</Form>
);
};
export default SignIn;
-253
View File
@@ -1,253 +0,0 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { Field, Form } from "react-final-form";
import { OnSubmit } from "talk-framework/lib/form";
import {
composeValidators,
required,
validateEmail,
validateEqualPasswords,
validatePassword,
validateUsername,
} from "talk-framework/lib/validation";
import {
Button,
CallOut,
Flex,
FormField,
HorizontalGutter,
InputDescription,
InputLabel,
TextField,
Typography,
ValidationMessage,
} from "talk-ui/components";
import AutoHeightContainer from "../containers/AutoHeightContainer";
interface FormProps {
email: string;
username: string;
password: string;
confirmPassword: string;
}
export interface SignUpForm {
onSubmit: OnSubmit<FormProps>;
onGotoSignIn: () => void;
}
const SignUp: StatelessComponent<SignUpForm> = props => {
return (
<Form onSubmit={props.onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<AutoHeightContainer />
<HorizontalGutter size="double">
<Localized id="signUp-signUpToJoinHeader">
<Typography variant="heading1" align="center">
Sign up to join the conversation
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<Field
name="email"
validate={composeValidators(required, validateEmail)}
>
{({ input, meta }) => (
<FormField>
<Localized id="signUp-emailAddressLabel">
<InputLabel>Email Address</InputLabel>
</Localized>
<Localized
id="signUp-emailAddressTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Email Address"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Field
name="username"
validate={composeValidators(required, validateUsername)}
>
{({ input, meta }) => (
<FormField>
<Localized id="signUp-usernameLabel">
<InputLabel>Username</InputLabel>
</Localized>
<Localized id="signUp-usernameDescription">
<InputDescription>
A unique identifier displayed on your comments. You may
use _ and .
</InputDescription>
</Localized>
<Localized
id="signUp-usernameTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Username"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Field
name="password"
validate={composeValidators(required, validatePassword)}
>
{({ input, meta }) => (
<FormField>
<Localized id="signUp-passwordLabel">
<InputLabel>Password</InputLabel>
</Localized>
<Localized id="signUp-passwordDescription" $minLength={8}>
<InputDescription>
{"Must be at least {$minLength} characters"}
</InputDescription>
</Localized>
<Localized
id="signUp-passwordTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Password"
type="password"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Field
name="confirmPassword"
validate={composeValidators(required, validateEqualPasswords)}
>
{({ input, meta }) => (
<FormField>
<Localized id="signUp-confirmPasswordLabel">
<InputLabel>Confirm Password</InputLabel>
</Localized>
<Localized
id="signUp-confirmPasswordTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Confirm Password"
type="password"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Localized id="signUp-signUpAndJoinButton">
<Button
variant="filled"
color="primary"
size="large"
type="submit"
disabled={submitting}
fullWidth
>
Sign up and join the conversation
</Button>
</Localized>
<Flex justifyContent="center">
<Localized
id="signUp-accountAvailableSignIn"
button={
<Button
id="signUp-gotoSignInButton"
variant="underlined"
size="small"
color="primary"
onClick={props.onGotoSignIn}
disabled={submitting}
/>
}
>
<Typography variant="bodyCopy" container={Flex}>
{"Already have an account? <button>Sign In </button>"}
</Typography>
</Localized>
</Flex>
</HorizontalGutter>
</form>
)}
</Form>
);
};
export default SignUp;
@@ -0,0 +1,62 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { Field } from "react-final-form";
import {
composeValidators,
required,
validateUsername,
} from "talk-framework/lib/validation";
import {
FormField,
InputDescription,
InputLabel,
TextField,
ValidationMessage,
} from "talk-ui/components";
interface Props {
disabled: boolean;
}
const CreateUsernameField: StatelessComponent<Props> = props => (
<Field
name="username"
validate={composeValidators(required, validateUsername)}
>
{({ input, meta }) => (
<FormField>
<Localized id="general-usernameLabel">
<InputLabel htmlFor={input.name}>Username</InputLabel>
</Localized>
<Localized id="general-usernameDescription">
<InputDescription>You may use _ and .</InputDescription>
</Localized>
<Localized id="general-usernameTextField" attrs={{ placeholder: true }}>
<TextField
id={input.name}
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Username"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={props.disabled}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
);
export default CreateUsernameField;
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders sign in 1`] = `
<div
className="App-root"
>
<withContext(createMutationContainer(withContext(createMutationContainer(SignInContainer)))) />
<div>
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(SignInContainer)))))
auth={Object {}}
/>
</div>
`;
@@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Button)
classes={
Object {
"active": "FacebookButton-active",
"colorRegular": "FacebookButton-colorRegular",
"disabled)": "FacebookButton-disabled)",
"mouseHover": "FacebookButton-mouseHover",
"variantFilled": "FacebookButton-variantFilled",
}
}
fullWidth={true}
onClick={[Function]}
size="large"
variant="filled"
>
<svg
fill="none"
height="17"
viewBox="0 0 17 17"
width="17"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.0893 1.25391C16.0893 1.00781 15.9838 0.796875 15.808 0.621094C15.6323 0.480469 15.4213 0.375 15.2104 0.375H1.21819C0.936942 0.375 0.726005 0.480469 0.58538 0.621094C0.409598 0.796875 0.339286 1.00781 0.339286 1.25391V15.2461C0.339286 15.4922 0.409598 15.7031 0.58538 15.8789C0.726005 16.0547 0.936942 16.125 1.21819 16.125H8.74163V10.0078H6.70257V7.65234H8.74163V5.89453C8.74163 4.91016 9.02288 4.13672 9.58538 3.57422C10.1479 3.04688 10.8862 2.76562 11.8002 2.76562C12.5033 2.76562 13.1362 2.80078 13.6283 2.83594V4.98047H12.3627C11.9057 4.98047 11.5893 5.08594 11.4135 5.29688C11.2729 5.47266 11.2026 5.75391 11.2026 6.14062V7.65234H13.558L13.2416 10.0078H11.2026V16.125H15.2104C15.4565 16.125 15.6674 16.0547 15.8432 15.8789C15.9838 15.7031 16.0893 15.4922 16.0893 15.2461V1.25391Z"
fill="white"
/>
</svg>
<span>
Login with Facebook
</span>
</withPropsOnChange(Button)>
`;
@@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Button)
classes={
Object {
"active": "GoogleButton-active",
"colorRegular": "GoogleButton-colorRegular",
"disabled)": "GoogleButton-disabled)",
"mouseHover": "GoogleButton-mouseHover",
"variantFilled": "GoogleButton-variantFilled",
}
}
fullWidth={true}
onClick={[Function]}
size="large"
variant="filled"
>
<svg
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.2924 9.46094C17.2924 9.00391 17.2221 8.51172 17.1518 7.98438H8.85491V11.0078H13.8119C13.7065 11.5352 13.4955 12.0625 13.2143 12.5547C12.7924 13.1875 12.2651 13.7148 11.6323 14.0664C10.8237 14.5586 9.9096 14.7695 8.85491 14.7695C7.87054 14.7695 6.95647 14.5234 6.14788 14.0312C5.30413 13.5391 4.67132 12.8711 4.17913 12.0273C3.68694 11.1836 3.44085 10.2695 3.44085 9.25C3.44085 8.08984 3.75725 7.03516 4.39007 6.12109C4.95257 5.27734 5.726 4.64453 6.71038 4.22266C7.6596 3.80078 8.64397 3.66016 9.62835 3.80078C10.683 3.94141 11.5619 4.39844 12.3354 5.10156L14.6908 2.81641C13.0737 1.30469 11.1049 0.53125 8.85491 0.53125C7.27288 0.53125 5.83147 0.953125 4.49554 1.72656C3.1596 2.5 2.06975 3.55469 1.29632 4.89062C0.52288 6.22656 0.136161 7.70312 0.136161 9.25C0.136161 10.832 0.52288 12.2734 1.29632 13.6094C2.06975 14.9453 3.1596 16.0352 4.49554 16.8086C5.83147 17.582 7.27288 17.9688 8.85491 17.9688C10.5073 17.9688 11.9838 17.6172 13.2494 16.8789C14.5151 16.1758 15.5346 15.1562 16.2377 13.8555C16.9408 12.5898 17.2924 11.1133 17.2924 9.46094Z"
fill="white"
/>
</svg>
<span>
Login with Google
</span>
</withPropsOnChange(Button)>
`;
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="Main-root"
>
Hello World
</div>
`;
@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Button)
classes={
Object {
"active": "OIDCButton-active",
"colorRegular": "OIDCButton-colorRegular",
"disabled)": "OIDCButton-disabled)",
"mouseHover": "OIDCButton-mouseHover",
"variantFilled": "OIDCButton-variantFilled",
}
}
fullWidth={true}
onClick={[Function]}
size="large"
variant="filled"
>
<span>
Login with OIDC
</span>
</withPropsOnChange(Button)>
`;
@@ -0,0 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Flex)
alignItems="center"
className="OrSeparator-root"
justifyContent="center"
>
<hr
className="OrSeparator-hr"
/>
<Localized
id="general-orSeparator"
>
<div
className="OrSeparator-text"
>
Or
</div>
</Localized>
</withPropsOnChange(Flex)>
`;
@@ -0,0 +1,122 @@
import * as React from "react";
import { Component } from "react";
import { AccountCompletionContainer_auth as AuthData } from "talk-auth/__generated__/AccountCompletionContainer_auth.graphql";
import { AccountCompletionContainer_me as UserData } from "talk-auth/__generated__/AccountCompletionContainer_me.graphql";
import { AccountCompletionContainerLocal as Local } from "talk-auth/__generated__/AccountCompletionContainerLocal.graphql";
import {
CompleteAccountMutation,
SetViewMutation,
withCompleteAccountMutation,
withSetViewMutation,
} from "talk-auth/mutations";
import {
graphql,
withFragmentContainer,
withLocalStateContainer,
} from "talk-framework/lib/relay";
interface Props {
completeAccount: CompleteAccountMutation;
setView: SetViewMutation;
local: Local;
auth: AuthData;
me: UserData | null;
}
function handleAccountCompletion(props: Props) {
const {
local: { view, authToken },
me,
auth,
setView,
completeAccount,
} = props;
if (me) {
if (!me.email) {
if (view !== "ADD_EMAIL_ADDRESS") {
setView({ view: "ADD_EMAIL_ADDRESS" });
}
} else if (!me.username) {
if (view !== "CREATE_USERNAME") {
setView({ view: "CREATE_USERNAME" });
}
} else if (
!me.profiles.some(p => p.__typename === "LocalProfile") &&
auth.integrations.local.enabled &&
auth.integrations.local.targetFilter.stream
) {
if (view !== "CREATE_PASSWORD") {
setView({ view: "CREATE_PASSWORD" });
}
} else {
completeAccount({ authToken: authToken! });
return true;
}
}
return false;
}
interface State {
checkedCompletionStatus: boolean;
}
class AccountCompletionContainer extends Component<Props, State> {
public state = {
checkedCompletionStatus: false,
};
public componentDidMount() {
handleAccountCompletion(this.props);
this.setState({ checkedCompletionStatus: true });
}
public componentDidUpdate() {
handleAccountCompletion(this.props);
}
public render() {
const { children } = this.props;
// We skip first frame to check for completion status.
if (!this.state.checkedCompletionStatus) {
return null;
}
return <>{children}</>;
}
}
const enhanced = withLocalStateContainer(
graphql`
fragment AccountCompletionContainerLocal on Local {
authToken
view
}
`
)(
withFragmentContainer<Props>({
auth: graphql`
fragment AccountCompletionContainer_auth on Auth {
integrations {
local {
enabled
targetFilter {
stream
}
}
}
}
`,
me: graphql`
fragment AccountCompletionContainer_me on User {
username
email
profiles {
__typename
}
}
`,
})(
withSetViewMutation(withCompleteAccountMutation(AccountCompletionContainer))
)
);
export default enhanced;
@@ -1,18 +1,38 @@
import * as React from "react";
import { StatelessComponent } from "react";
import { Component } from "react";
import { AppContainer_auth as AuthData } from "talk-auth/__generated__/AppContainer_auth.graphql";
import { AppContainer_me as UserData } from "talk-auth/__generated__/AppContainer_me.graphql";
import { AppContainerLocal as Local } from "talk-auth/__generated__/AppContainerLocal.graphql";
import { graphql, withLocalStateContainer } from "talk-framework/lib/relay";
import {
graphql,
withFragmentContainer,
withLocalStateContainer,
} from "talk-framework/lib/relay";
import App from "../components/App";
import AccountCompletionContainer from "./AccountCompletionContainer";
interface InnerProps {
interface Props {
local: Local;
auth: AuthData;
me: UserData | null;
}
const AppContainer: StatelessComponent<InnerProps> = ({ local: { view } }) => {
return <App view={view} />;
};
class AppContainer extends Component<Props> {
public render() {
const {
local: { view },
auth,
me,
} = this.props;
return (
<AccountCompletionContainer auth={auth} me={me}>
<App view={view} auth={auth} />
</AccountCompletionContainer>
);
}
}
const enhanced = withLocalStateContainer(
graphql`
@@ -20,6 +40,21 @@ const enhanced = withLocalStateContainer(
view
}
`
)(AppContainer);
)(
withFragmentContainer<Props>({
auth: graphql`
fragment AppContainer_auth on Auth {
...SignInContainer_auth
...SignUpContainer_auth
...AccountCompletionContainer_auth
}
`,
me: graphql`
fragment AppContainer_me on User {
...AccountCompletionContainer_me
}
`,
})(AppContainer)
);
export default enhanced;
@@ -1,33 +0,0 @@
import { FORM_ERROR } from "final-form";
import React, { Component } from "react";
import SignUp, { SignUpForm } from "../components/SignUp";
import {
SetViewMutation,
SignUpMutation,
withSetViewMutation,
withSignUpMutation,
} from "../mutations";
interface SignUpContainerProps {
signUp: SignUpMutation;
setView: SetViewMutation;
}
class SignUpContainer extends Component<SignUpContainerProps> {
private onSubmit: SignUpForm["onSubmit"] = async (input, form) => {
try {
await this.props.signUp(input);
return form.reset();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
};
private goToSignIn = () => this.props.setView({ view: "SIGN_IN" });
public render() {
return <SignUp onSubmit={this.onSubmit} onGotoSignIn={this.goToSignIn} />;
}
}
const enhanced = withSetViewMutation(withSignUpMutation(SignUpContainer));
export default enhanced;
+1
View File
@@ -0,0 +1 @@
export { default as redirectOAuth2 } from "./redirectOAuth2";
@@ -0,0 +1,4 @@
export default function redirectOAuth2(redirectURL: string) {
sessionStorage.setItem("authRedirectBackTo", window.location.pathname);
window.location.href = redirectURL;
}
+2 -2
View File
@@ -4,10 +4,10 @@ import ReactDOM from "react-dom";
import { createManaged } from "talk-framework/lib/bootstrap";
import AppContainer from "./containers/AppContainer";
import resizePopup from "./dom/resizePopup";
import { initLocalState } from "./local";
import localesData from "./locales";
import AppQuery from "./queries/AppQuery";
/**
* Adapt popup height to current content every 100ms.
@@ -37,7 +37,7 @@ async function main() {
const Index: StatelessComponent = () => (
<ManagedTalkContextProvider>
<AppContainer />
<AppQuery />
</ManagedTalkContextProvider>
);
@@ -1,5 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`get auth token from url 1`] = `
"{
\\"client:root\\": {
\\"__id\\": \\"client:root\\",
\\"__typename\\": \\"__Root\\",
\\"local\\": {
\\"__ref\\": \\"client:root.local\\"
}
},
\\"client:root.local\\": {
\\"__id\\": \\"client:root.local\\",
\\"__typename\\": \\"Local\\",
\\"authToken\\": \\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIzMWIyNjU5MS00ZTlhLTQzODgtYTdmZi1lMWJkYzVkOTdjY2UifQ==\\",
\\"authJTI\\": \\"31b26591-4e9a-4388-a7ff-e1bdc5d97cce\\",
\\"loggedIn\\": true,
\\"network\\": {
\\"__ref\\": \\"client:root.local.network\\"
},
\\"view\\": \\"SIGN_IN\\"
},
\\"client:root.local.network\\": {
\\"__id\\": \\"client:root.local.network\\",
\\"__typename\\": \\"Network\\",
\\"isOffline\\": false
}
}"
`;
exports[`init local state 1`] = `
"{
\\"client:root\\": {
@@ -12,7 +40,19 @@ exports[`init local state 1`] = `
\\"client:root.local\\": {
\\"__id\\": \\"client:root.local\\",
\\"__typename\\": \\"Local\\",
\\"authToken\\": \\"\\",
\\"authExp\\": null,
\\"authJTI\\": null,
\\"loggedIn\\": false,
\\"network\\": {
\\"__ref\\": \\"client:root.local.network\\"
},
\\"view\\": \\"SIGN_IN\\"
},
\\"client:root.local.network\\": {
\\"__id\\": \\"client:root.local.network\\",
\\"__typename\\": \\"Network\\",
\\"isOffline\\": false
}
}"
`;
@@ -2,6 +2,7 @@ import { Environment, RecordSource } from "relay-runtime";
import { LOCAL_ID } from "talk-framework/lib/relay";
import {
createAuthToken,
createRelayEnvironment,
replaceHistoryLocation,
} from "talk-framework/testHelpers";
@@ -11,6 +12,10 @@ import initLocalState from "./initLocalState";
let environment: Environment;
let source: RecordSource;
const context = {
localStorage: window.localStorage,
};
beforeEach(() => {
source = new RecordSource();
environment = createRelayEnvironment({
@@ -19,17 +24,26 @@ beforeEach(() => {
});
});
it("init local state", () => {
initLocalState(environment);
it("init local state", async () => {
await initLocalState(environment, context as any);
expect(JSON.stringify(source.toJSON(), null, 2)).toMatchSnapshot();
});
it("set view from query", () => {
it("set view from query", async () => {
const view = "SIGN_UP";
const restoreHistoryLocation = replaceHistoryLocation(
`http://localhost/?view=${view}`
);
initLocalState(environment);
await initLocalState(environment, context as any);
expect(source.get(LOCAL_ID)!.view).toBe(view);
restoreHistoryLocation();
});
it("get auth token from url", async () => {
const restoreHistoryLocation = replaceHistoryLocation(
`http://localhost/#${createAuthToken()}`
);
await initLocalState(environment, context as any);
expect(JSON.stringify(source.toJSON(), null, 2)).toMatchSnapshot();
restoreHistoryLocation();
});
+23 -12
View File
@@ -1,28 +1,39 @@
import { commitLocalUpdate, Environment } from "relay-runtime";
import { parseQuery } from "talk-common/utils";
import {
createAndRetain,
LOCAL_ID,
LOCAL_TYPE,
} from "talk-framework/lib/relay";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { initLocalBaseState, LOCAL_ID } from "talk-framework/lib/relay";
function getAuthTokenFromHashAndClearIt() {
const authToken = window.location.hash
? window.location.hash.substr(1)
: null;
// Remove hash with token.
if (window.location.hash) {
window.history.replaceState(null, document.title, location.pathname);
}
return authToken;
}
/**
* Initializes the local state, before we start the App.
*/
export default async function initLocalState(environment: Environment) {
commitLocalUpdate(environment, s => {
const root = s.getRoot();
export default async function initLocalState(
environment: Environment,
context: TalkContext
) {
const authToken = getAuthTokenFromHashAndClearIt();
await initLocalBaseState(environment, context, authToken);
// Create the Local Record which is the Root for the client states.
const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE);
commitLocalUpdate(environment, s => {
const localRecord = s.get(LOCAL_ID)!;
// Parse query params
const query = parseQuery(location.search);
// Set default view.
localRecord.setValue(query.view || "SIGN_IN", "view");
root.setLinkedRecord(localRecord, "local");
});
}
+3
View File
@@ -3,6 +3,9 @@ enum View {
SIGN_IN
FORGOT_PASSWORD
RESET_PASSWORD
CREATE_USERNAME
CREATE_PASSWORD
ADD_EMAIL_ADDRESS
}
type Local {
@@ -0,0 +1,25 @@
import { Environment } from "relay-runtime";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { createMutationContainer } from "talk-framework/lib/relay";
export interface CompleteAccountInput {
authToken: string;
}
export type CompleteAccountMutation = (
input: CompleteAccountInput
) => Promise<void>;
export async function commit(
environment: Environment,
input: CompleteAccountInput,
{ postMessage }: TalkContext
) {
postMessage.send("setAuthToken", input.authToken, window.opener);
window.close();
}
export const withCompleteAccountMutation = createMutationContainer(
"completeAccount",
commit
);
@@ -0,0 +1,46 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { SetEmailMutation as MutationTypes } from "talk-auth/__generated__/SetEmailMutation.graphql";
export type SetEmailInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
const mutation = graphql`
mutation SetEmailMutation($input: SetEmailInput!) {
setEmail(input: $input) {
user {
email
}
clientMutationId
}
}
`;
let clientMutationId = 0;
function commit(environment: Environment, input: SetEmailInput) {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
input: {
...input,
clientMutationId: (clientMutationId++).toString(),
},
},
});
}
export const withSetEmailMutation = createMutationContainer("setEmail", commit);
export type SetEmailMutation = (
input: SetEmailInput
) => Promise<MutationTypes["response"]["setEmail"]>;
@@ -0,0 +1,51 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { SetPasswordMutation as MutationTypes } from "talk-auth/__generated__/SetPasswordMutation.graphql";
export type SetPasswordInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
const mutation = graphql`
mutation SetPasswordMutation($input: SetPasswordInput!) {
setPassword(input: $input) {
user {
profiles {
__typename
}
}
clientMutationId
}
}
`;
let clientMutationId = 0;
function commit(environment: Environment, input: SetPasswordInput) {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
input: {
...input,
clientMutationId: (clientMutationId++).toString(),
},
},
});
}
export const withSetPasswordMutation = createMutationContainer(
"setPassword",
commit
);
export type SetPasswordMutation = (
input: SetPasswordInput
) => Promise<MutationTypes["response"]["setPassword"]>;
@@ -0,0 +1,49 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { SetUsernameMutation as MutationTypes } from "talk-auth/__generated__/SetUsernameMutation.graphql";
export type SetUsernameInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
const mutation = graphql`
mutation SetUsernameMutation($input: SetUsernameInput!) {
setUsername(input: $input) {
user {
username
}
clientMutationId
}
}
`;
let clientMutationId = 0;
function commit(environment: Environment, input: SetUsernameInput) {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
input: {
...input,
clientMutationId: (clientMutationId++).toString(),
},
},
});
}
export const withSetUsernameMutation = createMutationContainer(
"setUsername",
commit
);
export type SetUsernameMutation = (
input: SetUsernameInput
) => Promise<MutationTypes["response"]["setUsername"]>;
@@ -6,7 +6,14 @@ import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
export interface SetViewInput {
// TODO: replace with generated typescript types.
view: "SIGN_IN" | "SIGN_UP" | "FORGOT_PASSWORD" | "RESET_PASSWORD";
view:
| "SIGN_IN"
| "SIGN_UP"
| "FORGOT_PASSWORD"
| "RESET_PASSWORD"
| "ADD_EMAIL_ADDRESS"
| "CREATE_USERNAME"
| "CREATE_PASSWORD";
}
export type SetViewMutation = (input: SetViewInput) => Promise<void>;
@@ -1,3 +1,4 @@
import { pick } from "lodash";
import { Environment } from "relay-runtime";
import { TalkContext } from "talk-framework/lib/bootstrap";
@@ -9,16 +10,13 @@ export type SignInMutation = (input: SignInInput) => Promise<void>;
export async function commit(
environment: Environment,
input: SignInInput,
{ rest, postMessage }: TalkContext
{ rest, clearSession }: TalkContext
) {
try {
const result = await signIn(rest, input);
postMessage.send("setAuthToken", result.token, window.opener);
window.close();
} catch (err) {
postMessage.send("authError", err.toString(), window.opener);
throw err;
}
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 = result.token;
clearSession();
}
export const withSignInMutation = createMutationContainer("signIn", commit);
@@ -10,19 +10,16 @@ export type SignUpMutation = (input: SignUpInput) => Promise<void>;
export async function commit(
environment: Environment,
input: SignUpInput,
{ rest, postMessage }: TalkContext
{ rest, clearSession }: TalkContext
) {
try {
const result = await signUp(
rest,
pick(input, "email", "password", "username")
);
postMessage.send("setAuthToken", result.token, window.opener);
window.close();
} catch (err) {
postMessage.send("authError", err.toString(), window.opener);
throw err;
}
const result = await signUp(
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 = result.token;
clearSession();
}
export const withSignUpMutation = createMutationContainer("signUp", commit);
+13
View File
@@ -1,3 +1,16 @@
export { withSetViewMutation, SetViewMutation } from "./SetViewMutation";
export { withSignInMutation, SignInMutation } from "./SignInMutation";
export {
withCompleteAccountMutation,
CompleteAccountMutation,
} from "./CompleteAccountMutation";
export { withSignUpMutation, SignUpMutation } from "./SignUpMutation";
export { withSetEmailMutation, SetEmailMutation } from "./SetEmailMutation";
export {
withSetUsernameMutation,
SetUsernameMutation,
} from "./SetUsernameMutation";
export {
withSetPasswordMutation,
SetPasswordMutation,
} from "./SetPasswordMutation";
+37
View File
@@ -0,0 +1,37 @@
import React, { Component } from "react";
import { graphql, QueryRenderer } from "talk-framework/lib/relay";
import { AppQuery as QueryTypes } from "talk-auth/__generated__/AppQuery.graphql";
import AppContainer from "../containers/AppContainer";
export default class AppQuery extends Component {
public render() {
return (
<QueryRenderer<QueryTypes>
query={graphql`
query AppQuery {
me {
...AppContainer_me
}
settings {
auth {
...AppContainer_auth
}
}
}
`}
variables={{}}
render={({ error, props }) => {
if (error) {
return <div>{error.message}</div>;
}
if (!props) {
return null;
}
return <AppContainer auth={props.settings.auth} me={props.me} />;
}}
/>
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,446 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`checks for invalid password 1`] = `
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
To protect against unauthorized changes to your account,
we require users to create a password.
</p>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
htmlFor="password"
>
Password
</label>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
>
<div
className="PasswordField-wrapper"
>
<input
className="PasswordField-colorError PasswordField-fullWidth PasswordField-input"
disabled={false}
id="password"
name="password"
onChange={[Function]}
placeholder="Password"
type="password"
value="x"
/>
<div
className="PasswordField-icon"
onClick={[Function]}
role="button"
tabIndex={0}
title="Show password"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
visibility
</span>
</div>
</div>
</div>
<div
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
<span>
Password must contain at least 8 characters.
</span>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Create Password
</button>
</div>
</form>
`;
exports[`renders createPassword view 1`] = `
<div>
<div
data-testid="createPassword-container"
>
<div
className="Flex-root Bar-root Flex-flex Flex-justifyCenter Flex-alignCenter"
>
<div>
<h1
className="Typography-root Typography-heading2 Typography-colorTextPrimary Typography-alignCenter Title-root"
>
Create Password
</h1>
</div>
</div>
<div
className="Main-root"
data-testid="createPassword-main"
>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
To protect against unauthorized changes to your account,
we require users to create a password.
</p>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
htmlFor="password"
>
Password
</label>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
>
<div
className="PasswordField-wrapper"
>
<input
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
disabled={false}
id="password"
name="password"
onChange={[Function]}
placeholder="Password"
type="password"
value=""
/>
<div
className="PasswordField-icon"
onClick={[Function]}
role="button"
tabIndex={0}
title="Show password"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
visibility
</span>
</div>
</div>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Create Password
</button>
</div>
</form>
</div>
</div>
</div>
`;
exports[`shows error when submitting empty form 1`] = `
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
To protect against unauthorized changes to your account,
we require users to create a password.
</p>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
htmlFor="password"
>
Password
</label>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
>
<div
className="PasswordField-wrapper"
>
<input
className="PasswordField-colorError PasswordField-fullWidth PasswordField-input"
disabled={false}
id="password"
name="password"
onChange={[Function]}
placeholder="Password"
type="password"
value=""
/>
<div
className="PasswordField-icon"
onClick={[Function]}
role="button"
tabIndex={0}
title="Show password"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
visibility
</span>
</div>
</div>
</div>
<div
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
<span>
This field is required.
</span>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Create Password
</button>
</div>
</form>
`;
exports[`shows server error 1`] = `
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
To protect against unauthorized changes to your account,
we require users to create a password.
</p>
<div
className="CallOut-root CallOut-colorError CallOut-fullWidth"
>
server error
GraphQL request (4:3)
3: ) {
4: setPassword(input: $input) {
^
5: user {
</div>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
htmlFor="password"
>
Password
</label>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
>
<div
className="PasswordField-wrapper"
>
<input
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
disabled={false}
id="password"
name="password"
onChange={[Function]}
placeholder="Password"
type="password"
value="secretpassword"
/>
<div
className="PasswordField-icon"
onClick={[Function]}
role="button"
tabIndex={0}
title="Show password"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
visibility
</span>
</div>
</div>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Create Password
</button>
</div>
</form>
`;
exports[`successfully sets password 1`] = `
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
To protect against unauthorized changes to your account,
we require users to create a password.
</p>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
htmlFor="password"
>
Password
</label>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
>
<div
className="PasswordField-wrapper"
>
<input
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
disabled={false}
id="password"
name="password"
onChange={[Function]}
placeholder="Password"
type="password"
value=""
/>
<div
className="PasswordField-icon"
onClick={[Function]}
role="button"
tabIndex={0}
title="Show password"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
visibility
</span>
</div>
</div>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Create Password
</button>
</div>
</form>
`;
@@ -0,0 +1,385 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`accepts valid username 1`] = `
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Your username is a unique identifier that will appear on all of your comments.
</p>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
htmlFor="username"
>
Username
</label>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
You may use “_” and “.” Spaces not permitted.
</p>
<input
className="TextField-root TextField-colorRegular TextField-fullWidth"
disabled={true}
id="username"
name="username"
onChange={[Function]}
placeholder="Username"
type="text"
value="hans"
/>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth Button-disabled"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Create Username
</button>
</div>
</form>
`;
exports[`checks for invalid username 1`] = `
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Your username is a unique identifier that will appear on all of your comments.
</p>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
htmlFor="username"
>
Username
</label>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
You may use “_” and “.” Spaces not permitted.
</p>
<input
className="TextField-root TextField-colorError TextField-fullWidth"
disabled={false}
id="username"
name="username"
onChange={[Function]}
placeholder="Username"
type="text"
value="x"
/>
<div
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
<span>
Username must contain at least 3 characters.
</span>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Create Username
</button>
</div>
</form>
`;
exports[`renders createUsername view 1`] = `
<div>
<div
data-testid="createUsername-container"
>
<div
className="Flex-root Bar-root Flex-flex Flex-justifyCenter Flex-alignCenter"
>
<div>
<h1
className="Typography-root Typography-heading2 Typography-colorTextPrimary Typography-alignCenter Title-root"
>
Create Username
</h1>
</div>
</div>
<div
className="Main-root"
data-testid="createUsername-main"
>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Your username is a unique identifier that will appear on all of your comments.
</p>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
htmlFor="username"
>
Username
</label>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
You may use “_” and “.” Spaces not permitted.
</p>
<input
className="TextField-root TextField-colorRegular TextField-fullWidth"
disabled={false}
id="username"
name="username"
onChange={[Function]}
placeholder="Username"
type="text"
value=""
/>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Create Username
</button>
</div>
</form>
</div>
</div>
</div>
`;
exports[`shows error when submitting empty form 1`] = `
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Your username is a unique identifier that will appear on all of your comments.
</p>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
htmlFor="username"
>
Username
</label>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
You may use “_” and “.” Spaces not permitted.
</p>
<input
className="TextField-root TextField-colorError TextField-fullWidth"
disabled={false}
id="username"
name="username"
onChange={[Function]}
placeholder="Username"
type="text"
value=""
/>
<div
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
<span>
This field is required.
</span>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Create Username
</button>
</div>
</form>
`;
exports[`shows server error 1`] = `
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Your username is a unique identifier that will appear on all of your comments.
</p>
<div
className="CallOut-root CallOut-colorError CallOut-fullWidth"
>
server error
GraphQL request (4:3)
3: ) {
4: setUsername(input: $input) {
^
5: user {
</div>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
htmlFor="username"
>
Username
</label>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
You may use “_” and “.” Spaces not permitted.
</p>
<input
className="TextField-root TextField-colorRegular TextField-fullWidth"
disabled={false}
id="username"
name="username"
onChange={[Function]}
placeholder="Username"
type="text"
value="hans"
/>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Create Username
</button>
</div>
</form>
`;
exports[`successfully sets username 1`] = `
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Your username is a unique identifier that will appear on all of your comments.
</p>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
htmlFor="username"
>
Username
</label>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
You may use “_” and “.” Spaces not permitted.
</p>
<input
className="TextField-root TextField-colorRegular TextField-fullWidth"
disabled={false}
id="username"
name="username"
onChange={[Function]}
placeholder="Username"
type="text"
value=""
/>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Create Username
</button>
</div>
</form>
`;
@@ -1,422 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`navigates to forgot password form 1`] = `
<div
className="App-root"
>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<h1
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-alignCenter"
>
Forgot Password
</h1>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Enter your email address below and we will send you a link to
reset your password.
</p>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
>
Email Address
</label>
<input
className="TextField-root TextField-colorRegular TextField-fullWidth"
disabled={false}
name="email"
onChange={[Function]}
placeholder="Email Address"
type="text"
value=""
/>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Send Email
</button>
</div>
</form>
</div>
`;
exports[`navigates to sign in form 1`] = `
<div
className="App-root"
>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<h1
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-alignCenter"
>
Sign in to join the conversation
</h1>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
>
Email Address
</label>
<input
className="TextField-root TextField-colorRegular TextField-fullWidth"
disabled={false}
name="email"
onChange={[Function]}
placeholder="Email Address"
type="text"
value=""
/>
</div>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
>
Password
</label>
<input
className="TextField-root TextField-colorRegular TextField-fullWidth"
disabled={false}
name="password"
onChange={[Function]}
placeholder="Password"
type="password"
value=""
/>
<div
className="Flex-root Flex-flex Flex-justifyFlexEnd"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
disabled={false}
id="signIn-gotoForgotPasswordButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Forgot your password?
</button>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Sign in to Join the Conversation
</button>
<div
className="Flex-root Flex-flex Flex-justifyCenter"
>
<div
className="Flex-root Typography-root Typography-bodyCopy Typography-colorTextPrimary Flex-flex"
>
<span>
Don't have an account? 
</span>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
disabled={false}
id="signIn-gotoSignUpButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign Up
</button>
</div>
</div>
</div>
</form>
</div>
`;
exports[`navigates to sign up form 1`] = `
<div
className="App-root"
>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<h1
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-alignCenter"
>
Sign up to join the conversation
</h1>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
>
Email Address
</label>
<input
className="TextField-root TextField-colorRegular TextField-fullWidth"
disabled={false}
name="email"
onChange={[Function]}
placeholder="Email Address"
type="text"
value=""
/>
</div>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
>
Username
</label>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
<input
className="TextField-root TextField-colorRegular TextField-fullWidth"
disabled={false}
name="username"
onChange={[Function]}
placeholder="Username"
type="text"
value=""
/>
</div>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
>
Password
</label>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
<input
className="TextField-root TextField-colorRegular TextField-fullWidth"
disabled={false}
name="password"
onChange={[Function]}
placeholder="Password"
type="password"
value=""
/>
</div>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
>
Confirm Password
</label>
<input
className="TextField-root TextField-colorRegular TextField-fullWidth"
disabled={false}
name="confirmPassword"
onChange={[Function]}
placeholder="Confirm Password"
type="password"
value=""
/>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Sign up and Join the Conversation
</button>
<div
className="Flex-root Flex-flex Flex-justifyCenter"
>
<div
className="Flex-root Typography-root Typography-bodyCopy Typography-colorTextPrimary Flex-flex"
>
<span>
Already have an account? 
</span>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
disabled={false}
id="signUp-gotoSignInButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign In
</button>
</div>
</div>
</div>
</form>
</div>
`;
exports[`renders sign in form 1`] = `
<div
className="App-root"
>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<h1
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-alignCenter"
>
Sign in to join the conversation
</h1>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
>
Email Address
</label>
<input
className="TextField-root TextField-colorRegular TextField-fullWidth"
disabled={false}
name="email"
onChange={[Function]}
placeholder="Email Address"
type="text"
value=""
/>
</div>
<div
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
>
Password
</label>
<input
className="TextField-root TextField-colorRegular TextField-fullWidth"
disabled={false}
name="password"
onChange={[Function]}
placeholder="Password"
type="password"
value=""
/>
<div
className="Flex-root Flex-flex Flex-justifyFlexEnd"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
disabled={false}
id="signIn-gotoForgotPasswordButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Forgot your password?
</button>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Sign in to Join the Conversation
</button>
<div
className="Flex-root Flex-flex Flex-justifyCenter"
>
<div
className="Flex-root Typography-root Typography-bodyCopy Typography-colorTextPrimary Flex-flex"
>
<span>
Don't have an account? 
</span>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
disabled={false}
id="signIn-gotoSignUpButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign Up
</button>
</div>
</div>
</div>
</form>
</div>
`;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,142 @@
import { get, merge } from "lodash";
import sinon from "sinon";
import {
createAuthToken,
wait,
waitForElement,
within,
} from "talk-framework/testHelpers";
import create from "./create";
import { settings } from "./fixtures";
import mockWindow from "./mockWindow";
let windowMock: ReturnType<typeof mockWindow>;
const authToken = createAuthToken();
async function createTestRenderer(
customResolver: any = {},
options: { muteNetworkErrors?: boolean; logNetwork?: boolean } = {}
) {
const resolvers = {
...customResolver,
Query: {
...customResolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
me: sinon
.stub()
.returns(
merge({ id: "me", profiles: [] }, get(customResolver, "Query.me"))
),
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: options.logNetwork,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
localRecord.setValue("CREATE_PASSWORD", "view");
localRecord.setValue(authToken, "authToken");
},
});
return {
context,
testRenderer,
root: testRenderer.root,
};
}
beforeEach(async () => {
windowMock = mockWindow();
});
afterEach(async () => {
await wait(() => expect(windowMock.resizeStub.called).toBe(true));
windowMock.restore();
});
it("renders addEmailAddress view", async () => {
const { root } = await createTestRenderer();
await waitForElement(() =>
within(root).getByTestID("addEmailAddress-container")
);
});
it("renders createUsername view", async () => {
const { root } = await createTestRenderer({
Query: {
me: {
email: "hans@test.com",
},
},
});
await waitForElement(() =>
within(root).getByTestID("createUsername-container")
);
});
it("renders createPassword view", async () => {
const { root } = await createTestRenderer({
Query: {
me: {
email: "hans@test.com",
username: "hans",
},
},
});
await waitForElement(() =>
within(root).getByTestID("createPassword-container")
);
});
it("do not render createPassword view when local auth is disabled", async () => {
await createTestRenderer({
Query: {
me: {
email: "hans@test.com",
username: "hans",
},
settings: {
auth: {
integrations: {
local: {
enabled: false,
},
},
},
},
},
});
// Wait till window is closed.
await wait(() => expect(windowMock.closeStub.called).toBe(true));
});
it("send back auth token", async () => {
const { context } = await createTestRenderer({
Query: {
me: {
email: "hans@test.com",
username: "hans",
profiles: [{ __typename: "LocalProfile" }],
},
},
});
const postMessageMock = sinon.mock(context.postMessage);
postMessageMock
.expects("send")
.withArgs("setAuthToken", authToken, window.opener)
.atLeast(1);
// Wait till window is closed.
await wait(() => expect(windowMock.closeStub.called).toBe(true));
postMessageMock.verify();
});
@@ -0,0 +1,195 @@
import { get, merge } from "lodash";
import sinon from "sinon";
import {
toJSON,
wait,
waitForElement,
within,
} from "talk-framework/testHelpers";
import create from "./create";
import { settings } from "./fixtures";
import mockWindow from "./mockWindow";
let windowMock: ReturnType<typeof mockWindow>;
async function createTestRenderer(
customResolver: any = {},
options: { muteNetworkErrors?: boolean; logNetwork?: boolean } = {}
) {
const resolvers = {
...customResolver,
Query: {
...customResolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: options.logNetwork,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
localRecord.setValue("ADD_EMAIL_ADDRESS", "view");
},
});
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("addEmailAddress-container")
);
const main = within(testRenderer.root).getByTestID(/.*-main/);
const form = within(main).getByType("form");
const emailAddressField = within(form).getByLabelText("Email Address");
const confirmEmailAddressField = within(form).getByLabelText(
"Confirm Email Address"
);
return {
context,
testRenderer,
form,
main,
root: testRenderer.root,
emailAddressField,
confirmEmailAddressField,
container,
};
}
beforeEach(async () => {
windowMock = mockWindow();
});
afterEach(async () => {
await wait(() => expect(windowMock.resizeStub.called).toBe(true));
windowMock.restore();
});
it("renders addEmailAddress view", async () => {
const { root } = await createTestRenderer();
expect(toJSON(root)).toMatchSnapshot();
});
it("shows error when submitting empty form", async () => {
const { form } = await createTestRenderer();
form.props.onSubmit();
expect(toJSON(form)).toMatchSnapshot();
});
it("checks for invalid email", async () => {
const {
form,
emailAddressField,
confirmEmailAddressField,
} = await createTestRenderer();
emailAddressField.props.onChange({ target: { value: "invalid-email" } });
confirmEmailAddressField.props.onChange({
target: { value: "invalid-confirmation-email" },
});
form.props.onSubmit();
expect(toJSON(form)).toMatchSnapshot();
});
it("accepts valid email", async () => {
const { form, emailAddressField } = await createTestRenderer();
emailAddressField.props.onChange({ target: { value: "hans@test.com" } });
form.props.onSubmit();
expect(toJSON(form)).toMatchSnapshot();
});
it("accepts valid email confirmation", async () => {
const {
form,
emailAddressField,
confirmEmailAddressField,
} = await createTestRenderer();
emailAddressField.props.onChange({ target: { value: "hans@test.com" } });
confirmEmailAddressField.props.onChange({
target: { value: "hans@test.com" },
});
form.props.onSubmit();
expect(toJSON(form)).toMatchSnapshot();
});
it("shows server error", async () => {
const email = "hans@test.com";
const setEmail = sinon.stub().callsFake((_: any, data: any) => {
throw new Error("server error");
});
const {
form,
emailAddressField,
confirmEmailAddressField,
} = await createTestRenderer(
{
Mutation: {
setEmail,
},
},
{ muteNetworkErrors: true }
);
const submitButton = form.find(
i => i.type === "button" && i.props.type === "submit"
);
emailAddressField.props.onChange({ target: { value: email } });
confirmEmailAddressField.props.onChange({
target: { value: email },
});
form.props.onSubmit();
expect(emailAddressField.props.disabled).toBe(true);
expect(confirmEmailAddressField.props.disabled).toBe(true);
expect(submitButton.props.disabled).toBe(true);
await wait(() => expect(submitButton.props.disabled).toBe(false));
expect(toJSON(form)).toMatchSnapshot();
});
it("successfully sets email", async () => {
const email = "hans@test.com";
const setEmail = sinon.stub().callsFake((_: any, data: any) => {
expect(data.input).toEqual({
email,
clientMutationId: data.input.clientMutationId,
});
return {
user: {
id: "me",
email,
},
clientMutationId: data.input.clientMutationId,
};
});
const {
form,
emailAddressField,
confirmEmailAddressField,
} = await createTestRenderer({
Mutation: {
setEmail,
},
});
const submitButton = form.find(
i => i.type === "button" && i.props.type === "submit"
);
emailAddressField.props.onChange({ target: { value: email } });
confirmEmailAddressField.props.onChange({
target: { value: email },
});
form.props.onSubmit();
expect(emailAddressField.props.disabled).toBe(true);
expect(confirmEmailAddressField.props.disabled).toBe(true);
expect(submitButton.props.disabled).toBe(true);
await wait(() => expect(submitButton.props.disabled).toBe(false));
expect(toJSON(form)).toMatchSnapshot();
expect(setEmail.called).toBe(true);
});
+4 -2
View File
@@ -5,7 +5,7 @@ import React from "react";
import TestRenderer from "react-test-renderer";
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
import AppContainer from "talk-auth/containers/AppContainer";
import AppQuery from "talk-auth/queries/AppQuery";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import { PostMessageService } from "talk-framework/lib/postMessage";
import { RestClient } from "talk-framework/lib/rest";
@@ -17,6 +17,7 @@ import createFluentBundle from "./createFluentBundle";
interface CreateParams {
logNetwork?: boolean;
muteNetworkErrors?: boolean;
resolvers?: IResolvers<any, any>;
initLocalState?: (
local: RecordProxy,
@@ -29,6 +30,7 @@ export default function create(params: CreateParams) {
const environment = createEnvironment({
// Set this to true, to see graphql responses.
logNetwork: params.logNetwork,
muteNetworkErrors: params.muteNetworkErrors,
resolvers: params.resolvers,
initLocalState: (localRecord, source, env) => {
if (params.initLocalState) {
@@ -52,7 +54,7 @@ export default function create(params: CreateParams) {
const testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>
<AppContainer />
<AppQuery />
</TalkContextProvider>
);
@@ -4,6 +4,7 @@ import { createRelayEnvironment } from "talk-framework/testHelpers";
interface CreateEnvironmentParams {
logNetwork?: boolean;
muteNetworkErrors?: boolean;
resolvers?: IResolvers<any, any>;
initLocalState?: (
local: RecordProxy,
@@ -16,6 +17,7 @@ export default function createEnvironment(params: CreateEnvironmentParams) {
return createRelayEnvironment({
network: {
logNetwork: params.logNetwork,
muteNetworkErrors: params.muteNetworkErrors,
resolvers: params.resolvers || {},
projectName: "tenant",
},
@@ -0,0 +1,147 @@
import { get, merge } from "lodash";
import sinon from "sinon";
import {
toJSON,
wait,
waitForElement,
within,
} from "talk-framework/testHelpers";
import create from "./create";
import { settings } from "./fixtures";
import mockWindow from "./mockWindow";
let windowMock: ReturnType<typeof mockWindow>;
async function createTestRenderer(
customResolver: any = {},
options: { muteNetworkErrors?: boolean; logNetwork?: boolean } = {}
) {
const resolvers = {
...customResolver,
Query: {
...customResolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: options.logNetwork,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
localRecord.setValue("CREATE_PASSWORD", "view");
},
});
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("createPassword-container")
);
const main = within(testRenderer.root).getByTestID(/.*-main/);
const form = within(main).getByType("form");
const passwordField = within(form).getByLabelText("Password");
return {
context,
testRenderer,
form,
main,
root: testRenderer.root,
passwordField,
container,
};
}
beforeEach(async () => {
windowMock = mockWindow();
});
afterEach(async () => {
await wait(() => expect(windowMock.resizeStub.called).toBe(true));
windowMock.restore();
});
it("renders createPassword view", async () => {
const { root } = await createTestRenderer();
expect(toJSON(root)).toMatchSnapshot();
});
it("shows error when submitting empty form", async () => {
const { form } = await createTestRenderer();
form.props.onSubmit();
expect(toJSON(form)).toMatchSnapshot();
});
it("checks for invalid password", async () => {
const { form, passwordField } = await createTestRenderer();
passwordField.props.onChange({ target: { value: "x" } });
form.props.onSubmit();
expect(toJSON(form)).toMatchSnapshot();
});
it("shows server error", async () => {
const password = "secretpassword";
const setPassword = sinon.stub().callsFake((_: any, data: any) => {
throw new Error("server error");
});
const { form, passwordField } = await createTestRenderer(
{
Mutation: {
setPassword,
},
},
{ muteNetworkErrors: true }
);
const submitButton = form.find(
i => i.type === "button" && i.props.type === "submit"
);
passwordField.props.onChange({ target: { value: password } });
form.props.onSubmit();
expect(passwordField.props.disabled).toBe(true);
expect(submitButton.props.disabled).toBe(true);
await wait(() => expect(submitButton.props.disabled).toBe(false));
expect(toJSON(form)).toMatchSnapshot();
});
it("successfully sets password", async () => {
const password = "secretpassword";
const setPassword = sinon.stub().callsFake((_: any, data: any) => {
expect(data.input).toEqual({
password,
clientMutationId: data.input.clientMutationId,
});
return {
user: {
id: "me",
profiles: [],
},
clientMutationId: data.input.clientMutationId,
};
});
const { form, passwordField } = await createTestRenderer({
Mutation: {
setPassword,
},
});
const submitButton = form.find(
i => i.type === "button" && i.props.type === "submit"
);
passwordField.props.onChange({ target: { value: password } });
form.props.onSubmit();
expect(passwordField.props.disabled).toBe(true);
expect(submitButton.props.disabled).toBe(true);
await wait(() => expect(submitButton.props.disabled).toBe(false));
expect(toJSON(form)).toMatchSnapshot();
expect(setPassword.called).toBe(true);
});
@@ -0,0 +1,154 @@
import { get, merge } from "lodash";
import sinon from "sinon";
import {
toJSON,
wait,
waitForElement,
within,
} from "talk-framework/testHelpers";
import create from "./create";
import { settings } from "./fixtures";
import mockWindow from "./mockWindow";
let windowMock: ReturnType<typeof mockWindow>;
async function createTestRenderer(
customResolver: any = {},
options: { muteNetworkErrors?: boolean; logNetwork?: boolean } = {}
) {
const resolvers = {
...customResolver,
Query: {
...customResolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: options.logNetwork,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
localRecord.setValue("CREATE_USERNAME", "view");
},
});
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("createUsername-container")
);
const main = within(testRenderer.root).getByTestID(/.*-main/);
const form = within(main).getByType("form");
const usernameField = within(form).getByLabelText("Username");
return {
context,
testRenderer,
form,
main,
root: testRenderer.root,
usernameField,
container,
};
}
beforeEach(async () => {
windowMock = mockWindow();
});
afterEach(async () => {
await wait(() => expect(windowMock.resizeStub.called).toBe(true));
windowMock.restore();
});
it("renders createUsername view", async () => {
const { root } = await createTestRenderer();
expect(toJSON(root)).toMatchSnapshot();
});
it("shows error when submitting empty form", async () => {
const { form } = await createTestRenderer();
form.props.onSubmit();
expect(toJSON(form)).toMatchSnapshot();
});
it("checks for invalid username", async () => {
const { form, usernameField } = await createTestRenderer();
usernameField.props.onChange({ target: { value: "x" } });
form.props.onSubmit();
expect(toJSON(form)).toMatchSnapshot();
});
it("accepts valid username", async () => {
const { form, usernameField } = await createTestRenderer();
usernameField.props.onChange({ target: { value: "hans" } });
form.props.onSubmit();
expect(toJSON(form)).toMatchSnapshot();
});
it("shows server error", async () => {
const username = "hans";
const setUsername = sinon.stub().callsFake((_: any, data: any) => {
throw new Error("server error");
});
const { form, usernameField } = await createTestRenderer(
{
Mutation: {
setUsername,
},
},
{ muteNetworkErrors: true }
);
const submitButton = form.find(
i => i.type === "button" && i.props.type === "submit"
);
usernameField.props.onChange({ target: { value: username } });
form.props.onSubmit();
expect(usernameField.props.disabled).toBe(true);
expect(submitButton.props.disabled).toBe(true);
await wait(() => expect(submitButton.props.disabled).toBe(false));
expect(toJSON(form)).toMatchSnapshot();
});
it("successfully sets username", async () => {
const username = "hans";
const setUsername = sinon.stub().callsFake((_: any, data: any) => {
expect(data.input).toEqual({
username,
clientMutationId: data.input.clientMutationId,
});
return {
user: {
id: "me",
username,
},
clientMutationId: data.input.clientMutationId,
};
});
const { form, usernameField } = await createTestRenderer({
Mutation: {
setUsername,
},
});
const submitButton = form.find(
i => i.type === "button" && i.props.type === "submit"
);
usernameField.props.onChange({ target: { value: username } });
form.props.onSubmit();
expect(usernameField.props.disabled).toBe(true);
expect(submitButton.props.disabled).toBe(true);
await wait(() => expect(submitButton.props.disabled).toBe(false));
expect(toJSON(form)).toMatchSnapshot();
expect(setUsername.called).toBe(true);
});
+51
View File
@@ -0,0 +1,51 @@
export const settings = {
id: "settings",
auth: {
integrations: {
facebook: {
enabled: false,
allowRegistration: true,
targetFilter: {
stream: true,
admin: true,
},
redirectURL: "http://localhost/facebook",
},
google: {
enabled: false,
allowRegistration: true,
targetFilter: {
stream: true,
admin: true,
},
redirectURL: "http://localhost/google",
},
sso: {
enabled: false,
allowRegistration: true,
targetFilter: {
stream: true,
admin: true,
},
},
oidc: {
enabled: false,
allowRegistration: true,
targetFilter: {
stream: true,
admin: true,
},
name: "OIDC",
redirectURL: "http://localhost/oidc",
},
local: {
enabled: true,
allowRegistration: true,
targetFilter: {
stream: true,
admin: true,
},
},
},
},
};
+18
View File
@@ -0,0 +1,18 @@
import sinon from "sinon";
export default function mockWindow() {
const originalClose = window.close;
const originalResizeTo = window.resizeTo;
const closeStub = sinon.stub();
const resizeStub = sinon.stub();
window.close = closeStub;
window.resizeTo = resizeStub;
return {
closeStub,
resizeStub,
restore: () => {
window.close = originalClose;
window.resizeTo = originalResizeTo;
},
};
}
+55 -15
View File
@@ -1,11 +1,24 @@
import { ReactTestRenderer } from "react-test-renderer";
import sinon from "sinon";
import { wait, waitForElement, within } from "talk-framework/testHelpers";
import create from "./create";
import { settings } from "./fixtures";
import mockWindow from "./mockWindow";
function createTestRenderer(initialView: string): ReactTestRenderer {
async function createTestRenderer(
initialView: string
): Promise<ReactTestRenderer> {
const resolvers = {
Query: {
settings: sinon.stub().returns(settings),
},
};
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(initialView, "view");
},
@@ -13,31 +26,58 @@ function createTestRenderer(initialView: string): ReactTestRenderer {
return testRenderer;
}
let windowMock: ReturnType<typeof mockWindow>;
beforeEach(() => {
windowMock = mockWindow();
});
afterEach(async () => {
await wait(() => expect(windowMock.resizeStub.called).toBe(true));
windowMock.restore();
});
it("renders sign in form", async () => {
const testRenderer = createTestRenderer("SIGN_IN");
expect(testRenderer.toJSON()).toMatchSnapshot();
const testRenderer = await createTestRenderer("SIGN_IN");
await waitForElement(() =>
within(testRenderer.root).getByTestID("signIn-container")
);
});
it("navigates to sign up form", async () => {
const testRenderer = createTestRenderer("SIGN_IN");
testRenderer.root
.findByProps({ id: "signIn-gotoSignUpButton" })
const testRenderer = await createTestRenderer("SIGN_IN");
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("signIn-container")
);
within(container)
.getByTestID("gotoSignUpButton")
.props.onClick();
expect(testRenderer.toJSON()).toMatchSnapshot();
await waitForElement(() =>
within(testRenderer.root).getByTestID("signUp-container")
);
});
it("navigates to sign in form", async () => {
const testRenderer = createTestRenderer("SIGN_UP");
testRenderer.root
.findByProps({ id: "signUp-gotoSignInButton" })
const testRenderer = await createTestRenderer("SIGN_UP");
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("signUp-container")
);
within(container)
.getByTestID("gotoSignInButton")
.props.onClick();
expect(testRenderer.toJSON()).toMatchSnapshot();
await waitForElement(() =>
within(testRenderer.root).getByTestID("signIn-container")
);
});
it("navigates to forgot password form", async () => {
const testRenderer = createTestRenderer("SIGN_IN");
testRenderer.root
.findByProps({ id: "signIn-gotoForgotPasswordButton" })
const testRenderer = await createTestRenderer("SIGN_IN");
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("signIn-container")
);
within(container)
.getByTestID("gotoForgotPasswordButton")
.props.onClick();
expect(testRenderer.toJSON()).toMatchSnapshot();
await waitForElement(() =>
within(testRenderer.root).getByTestID("forgotPassword-container")
);
});
+214 -75
View File
@@ -1,72 +1,111 @@
import { ReactTestInstance, ReactTestRenderer } from "react-test-renderer";
import { get, merge } from "lodash";
import sinon from "sinon";
import { animationFrame, timeout } from "talk-common/utils";
import { TalkContext } from "talk-framework/lib/bootstrap";
import {
toJSON,
wait,
waitForElement,
within,
} from "talk-framework/testHelpers";
import create from "./create";
import { settings } from "./fixtures";
import mockWindow from "./mockWindow";
const inputPredicate = (name: string) => (n: ReactTestInstance) => {
return n.props.name === name && n.props.onChange;
};
let windowMock: ReturnType<typeof mockWindow>;
let context: TalkContext;
let testRenderer: ReactTestRenderer;
let form: ReactTestInstance;
beforeEach(() => {
({ testRenderer, context } = create({
async function createTestRenderer(customResolver: any = {}) {
const resolvers = {
...customResolver,
Query: {
...customResolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
localRecord.setValue("SIGN_IN", "view");
},
}));
form = testRenderer.root.findByType("form");
});
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("signIn-container")
);
const main = within(testRenderer.root).getByTestID(/.*-main/);
const form = within(main).queryByType("form");
return {
context,
testRenderer,
form,
main,
container,
};
}
beforeEach(async () => {
windowMock = mockWindow();
});
it("renders sign in form", async () => {
afterEach(async () => {
await wait(() => expect(windowMock.resizeStub.called).toBe(true));
windowMock.restore();
});
it("renders sign in view", async () => {
const { testRenderer } = await createTestRenderer();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("shows error when submitting empty form", async () => {
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
const { form } = await createTestRenderer();
form!.props.onSubmit();
expect(toJSON(form!)).toMatchSnapshot();
});
it("checks for invalid email", async () => {
form
.find(inputPredicate("email"))
.props.onChange({ target: { value: "invalid-email" } });
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
const { form } = await createTestRenderer();
const { getByLabelText } = within(form!);
const emailAddressField = getByLabelText("Email Address");
emailAddressField.props.onChange({ target: { value: "invalid-email" } });
form!.props.onSubmit();
expect(toJSON(form!)).toMatchSnapshot();
});
it("accepts valid email", async () => {
form
.find(inputPredicate("email"))
.props.onChange({ target: { value: "hans@test.com" } });
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
const { form } = await createTestRenderer();
const { getByLabelText } = within(form!);
const emailAddressField = getByLabelText("Email Address");
emailAddressField.props.onChange({ target: { value: "hans@test.com" } });
form!.props.onSubmit();
expect(toJSON(form!)).toMatchSnapshot();
});
it("accepts correct password", async () => {
form
.find(inputPredicate("password"))
.props.onChange({ target: { value: "testtest" } });
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
const { form } = await createTestRenderer();
const { getByLabelText } = within(form!);
const passwordField = getByLabelText("Password");
passwordField.props.onChange({ target: { value: "testtest" } });
form!.props.onSubmit();
expect(toJSON(form!)).toMatchSnapshot();
});
it("shows server error", async () => {
form
.find(inputPredicate("email"))
.props.onChange({ target: { value: "hans@test.com" } });
form
.find(inputPredicate("password"))
.props.onChange({ target: { value: "testtest" } });
const { form, context } = await createTestRenderer();
const { getByLabelText } = within(form!);
const emailAddressField = getByLabelText("Email Address");
const passwordField = getByLabelText("Password");
const submitButton = form!.find(
i => i.type === "button" && i.props.type === "submit"
);
const windowMock = sinon.mock(window);
windowMock.expects("resizeTo");
passwordField.props.onChange({ target: { value: "testtest" } });
emailAddressField.props.onChange({ target: { value: "hans@test.com" } });
const error = new Error("Server Error");
const restMock = sinon.mock(context.rest);
@@ -82,34 +121,30 @@ it("shows server error", async () => {
.once()
.throws(error);
const postMessageMock = sinon.mock(context.postMessage);
postMessageMock
.expects("send")
.withArgs("authError", error.toString(), window.opener)
.once();
form!.props.onSubmit();
expect(emailAddressField.props.disabled).toBe(true);
expect(passwordField.props.disabled).toBe(true);
expect(submitButton.props.disabled).toBe(true);
await wait(() => expect(submitButton.props.disabled).toBe(false));
expect(toJSON(form!)).toMatchSnapshot();
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
// popup resize will be triggered if we wait for the animation frame first.
await animationFrame();
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
restMock.verify();
postMessageMock.verify();
windowMock.verify();
});
it("submits form successfully", async () => {
form
.find(inputPredicate("email"))
.props.onChange({ target: { value: "hans@test.com" } });
form
.find(inputPredicate("password"))
.props.onChange({ target: { value: "testtest" } });
const { form, context } = await createTestRenderer();
const { getByLabelText } = within(form!);
const authToken = "auth-token";
const emailAddressField = getByLabelText("Email Address");
const passwordField = getByLabelText("Password");
const submitButton = form!.find(
i => i.type === "button" && i.props.type === "submit"
);
const windowMock = sinon.mock(window);
windowMock.expects("close").once();
windowMock.expects("resizeTo");
emailAddressField.props.onChange({ target: { value: "hans@test.com" } });
passwordField.props.onChange({ target: { value: "testtest" } });
const restMock = sinon.mock(context.rest);
restMock
@@ -122,21 +157,125 @@ it("submits form successfully", async () => {
},
})
.once()
.returns({ token: "auth-token" });
.returns({ token: authToken });
const postMessageMock = sinon.mock(context.postMessage);
postMessageMock
.expects("send")
.withArgs("setAuthToken", "auth-token", window.opener)
.once();
form!.props.onSubmit();
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
// popup resize will be triggered if we wait for the animation frame first.
await animationFrame();
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
expect(emailAddressField.props.disabled).toBe(true);
expect(passwordField.props.disabled).toBe(true);
expect(submitButton.props.disabled).toBe(true);
await wait(() => expect(submitButton.props.disabled).toBe(false));
expect(toJSON(form!)).toMatchSnapshot();
// Wait for window hash to contain a token.
await wait(() => expect(location.hash).toBe(`#${authToken}`));
restMock.verify();
postMessageMock.verify();
windowMock.verify();
});
describe("auth configuration", () => {
it("renders all auth enabled", async () => {
const { main } = await createTestRenderer({
Query: {
settings: {
auth: {
integrations: {
facebook: {
enabled: true,
},
google: {
enabled: true,
},
oidc: {
enabled: true,
},
},
},
},
},
});
expect(toJSON(main)).toMatchSnapshot();
});
it("renders all social login disabled", async () => {
const { main } = await createTestRenderer({
Query: {
settings: {
auth: {
integrations: {
facebook: {
enabled: false,
},
google: {
enabled: false,
},
oidc: {
enabled: false,
},
},
},
},
},
});
const { queryByText } = within(main);
expect(queryByText("facebook")).toBeNull();
expect(queryByText("google")).toBeNull();
expect(queryByText("oidc")).toBeNull();
});
it("renders all social login disabled for stream target", async () => {
const { main } = await createTestRenderer({
Query: {
settings: {
auth: {
integrations: {
facebook: {
enabled: true,
targetFilter: {
stream: false,
},
},
google: {
enabled: true,
targetFilter: {
stream: false,
},
},
oidc: {
enabled: true,
targetFilter: {
stream: false,
},
},
},
},
},
},
});
const { queryByText } = within(main);
expect(queryByText("facebook")).toBeNull();
expect(queryByText("google")).toBeNull();
expect(queryByText("oidc")).toBeNull();
});
it("renders only some social login enabled", async () => {
const { main } = await createTestRenderer({
Query: {
settings: {
auth: {
integrations: {
local: {
enabled: false,
},
google: {
enabled: true,
},
facebook: {
enabled: true,
},
},
},
},
},
});
expect(toJSON(main)).toMatchSnapshot();
});
});
+278 -127
View File
@@ -1,134 +1,158 @@
import { ReactTestInstance, ReactTestRenderer } from "react-test-renderer";
import { get, merge } from "lodash";
import sinon from "sinon";
import { animationFrame, timeout } from "talk-common/utils";
import { TalkContext } from "talk-framework/lib/bootstrap";
import {
toJSON,
wait,
waitForElement,
within,
} from "talk-framework/testHelpers";
import create from "./create";
import { settings } from "./fixtures";
import mockWindow from "./mockWindow";
const inputPredicate = (name: string) => (n: ReactTestInstance) => {
return n.props.name === name && n.props.onChange;
};
let windowMock: ReturnType<typeof mockWindow>;
let context: TalkContext;
let testRenderer: ReactTestRenderer;
let form: ReactTestInstance;
beforeEach(() => {
({ testRenderer, context } = create({
async function createTestRenderer(customResolver: any = {}) {
const resolvers = {
...customResolver,
Query: {
...customResolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
localRecord.setValue("SIGN_UP", "view");
},
}));
form = testRenderer.root.findByType("form");
});
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("signUp-container")
);
const main = within(testRenderer.root).getByTestID(/.*-main/);
const form = within(main).queryByType("form");
return {
context,
testRenderer,
form,
main,
container,
};
}
beforeEach(async () => {
windowMock = mockWindow();
});
afterEach(async () => {
await wait(() => expect(windowMock.resizeStub.called).toBe(true));
windowMock.restore();
});
it("renders sign up form", async () => {
const { testRenderer } = await createTestRenderer();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("shows error when submitting empty form", async () => {
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
const { main, form } = await createTestRenderer();
form!.props.onSubmit();
expect(toJSON(main)).toMatchSnapshot();
});
it("checks for invalid email", async () => {
form
.find(inputPredicate("email"))
.props.onChange({ target: { value: "invalid-email" } });
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
const { main, form } = await createTestRenderer();
const { getByLabelText } = within(form!);
const emailAddressField = getByLabelText("Email Address");
emailAddressField.props.onChange({ target: { value: "invalid-email" } });
form!.props.onSubmit();
expect(toJSON(main)).toMatchSnapshot();
});
it("accepts valid email", async () => {
form
.find(inputPredicate("email"))
.props.onChange({ target: { value: "hans@test.com" } });
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
const { main, form } = await createTestRenderer();
const { getByLabelText } = within(form!);
const emailAddressField = getByLabelText("Email Address");
emailAddressField.props.onChange({ target: { value: "hans@test.com" } });
form!.props.onSubmit();
expect(toJSON(main)).toMatchSnapshot();
});
it("checks for too short username", async () => {
form
.find(inputPredicate("username"))
.props.onChange({ target: { value: "u" } });
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
const { main, form } = await createTestRenderer();
const { getByLabelText } = within(form!);
const usernameField = getByLabelText("Username");
usernameField.props.onChange({ target: { value: "u" } });
form!.props.onSubmit();
expect(toJSON(main)).toMatchSnapshot();
});
it("checks for too long username", async () => {
form
.find(inputPredicate("username"))
.props.onChange({ target: { value: "a".repeat(100) } });
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
const { main, form } = await createTestRenderer();
const { getByLabelText } = within(form!);
const usernameField = getByLabelText("Username");
usernameField.props.onChange({ target: { value: "a".repeat(100) } });
form!.props.onSubmit();
expect(toJSON(main)).toMatchSnapshot();
});
it("checks for invalid characters in username", async () => {
form
.find(inputPredicate("username"))
.props.onChange({ target: { value: "$%$§$%$§%" } });
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
const { main, form } = await createTestRenderer();
const { getByLabelText } = within(form!);
const usernameField = getByLabelText("Username");
usernameField.props.onChange({ target: { value: "$%$§$%$§%" } });
form!.props.onSubmit();
expect(toJSON(main)).toMatchSnapshot();
});
it("accepts valid username", async () => {
form
.find(inputPredicate("username"))
.props.onChange({ target: { value: "hans" } });
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
const { main, form } = await createTestRenderer();
const { getByLabelText } = within(form!);
const usernameField = getByLabelText("Username");
usernameField.props.onChange({ target: { value: "hans" } });
form!.props.onSubmit();
expect(toJSON(main)).toMatchSnapshot();
});
it("checks for too short password", async () => {
form
.find(inputPredicate("password"))
.props.onChange({ target: { value: "pass" } });
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
const { main, form } = await createTestRenderer();
const { getByLabelText } = within(form!);
const passwordField = getByLabelText("Password");
passwordField.props.onChange({ target: { value: "pass" } });
form!.props.onSubmit();
expect(toJSON(main)).toMatchSnapshot();
});
it("accepts correct password", async () => {
form
.find(inputPredicate("password"))
.props.onChange({ target: { value: "testtest" } });
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("checks for wrong password confirmation", async () => {
form
.find(inputPredicate("confirmPassword"))
.props.onChange({ target: { value: "not-matching" } });
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("accepts correct password confirmation", async () => {
form
.find(inputPredicate("confirmPassword"))
.props.onChange({ target: { value: "testtest" } });
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
const { main, form } = await createTestRenderer();
const { getByLabelText } = within(form!);
const passwordField = getByLabelText("Password");
passwordField.props.onChange({ target: { value: "testtest" } });
form!.props.onSubmit();
expect(toJSON(main)).toMatchSnapshot();
});
it("shows server error", async () => {
form
.find(inputPredicate("email"))
.props.onChange({ target: { value: "hans@test.com" } });
form
.find(inputPredicate("username"))
.props.onChange({ target: { value: "hans" } });
form
.find(inputPredicate("password"))
.props.onChange({ target: { value: "testtest" } });
form
.find(inputPredicate("confirmPassword"))
.props.onChange({ target: { value: "testtest" } });
const { context, main, form } = await createTestRenderer();
const { getByLabelText } = within(form!);
const emailAddressField = getByLabelText("Email Address");
const usernameField = getByLabelText("Username");
const passwordField = getByLabelText("Password");
const submitButton = form!.find(
i => i.type === "button" && i.props.type === "submit"
);
const windowMock = sinon.mock(window);
windowMock.expects("resizeTo");
emailAddressField.props.onChange({ target: { value: "hans@test.com" } });
usernameField.props.onChange({ target: { value: "hans" } });
passwordField.props.onChange({ target: { value: "testtest" } });
const error = new Error("Server Error");
const restMock = sinon.mock(context.rest);
@@ -145,40 +169,34 @@ it("shows server error", async () => {
.once()
.throws(error);
const postMessageMock = sinon.mock(context.postMessage);
postMessageMock
.expects("send")
.withArgs("authError", error.toString(), window.opener)
.once();
form!.props.onSubmit();
expect(emailAddressField.props.disabled).toBe(true);
expect(passwordField.props.disabled).toBe(true);
expect(usernameField.props.disabled).toBe(true);
expect(submitButton.props.disabled).toBe(true);
await wait(() => expect(submitButton.props.disabled).toBe(false));
expect(toJSON(main)).toMatchSnapshot();
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
// popup resize will be triggered if we wait for the animation frame first.
await animationFrame();
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
restMock.verify();
postMessageMock.verify();
windowMock.verify();
});
it("submits form successfully", async () => {
form
.find(inputPredicate("email"))
.props.onChange({ target: { value: "hans@test.com" } });
form
.find(inputPredicate("username"))
.props.onChange({ target: { value: "hans" } });
form
.find(inputPredicate("password"))
.props.onChange({ target: { value: "testtest" } });
form
.find(inputPredicate("confirmPassword"))
.props.onChange({ target: { value: "testtest" } });
const { context, main, form } = await createTestRenderer();
const { getByLabelText } = within(form!);
const authToken = "auth-token";
const emailAddressField = getByLabelText("Email Address");
const usernameField = getByLabelText("Username");
const passwordField = getByLabelText("Password");
const submitButton = form!.find(
i => i.type === "button" && i.props.type === "submit"
);
const windowMock = sinon.mock(window);
windowMock.expects("close").once();
windowMock.expects("resizeTo");
emailAddressField.props.onChange({ target: { value: "hans@test.com" } });
usernameField.props.onChange({ target: { value: "hans" } });
passwordField.props.onChange({ target: { value: "testtest" } });
const restMock = sinon.mock(context.rest);
restMock
@@ -192,21 +210,154 @@ it("submits form successfully", async () => {
},
})
.once()
.returns({ token: "auth-token" });
.returns({ token: authToken });
const postMessageMock = sinon.mock(context.postMessage);
postMessageMock
.expects("send")
.withArgs("setAuthToken", "auth-token", window.opener)
.once();
form!.props.onSubmit();
form.props.onSubmit();
expect(testRenderer.toJSON()).toMatchSnapshot();
// popup resize will be triggered if we wait for the animation frame first.
await animationFrame();
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
expect(emailAddressField.props.disabled).toBe(true);
expect(passwordField.props.disabled).toBe(true);
expect(usernameField.props.disabled).toBe(true);
expect(submitButton.props.disabled).toBe(true);
await wait(() => expect(submitButton.props.disabled).toBe(false));
expect(toJSON(main)).toMatchSnapshot();
// Wait for window hash to contain a token.
await wait(() => expect(location.hash).toBe(`#${authToken}`));
restMock.verify();
postMessageMock.verify();
windowMock.verify();
});
describe("auth configuration", () => {
it("renders all auth enabled", async () => {
const { main } = await createTestRenderer({
Query: {
settings: {
auth: {
integrations: {
facebook: {
enabled: true,
},
google: {
enabled: true,
},
oidc: {
enabled: true,
},
},
},
},
},
});
expect(toJSON(main)).toMatchSnapshot();
});
it("renders all social login disabled", async () => {
const { main } = await createTestRenderer({
Query: {
settings: {
auth: {
integrations: {
facebook: {
enabled: false,
},
google: {
enabled: false,
},
oidc: {
enabled: false,
},
},
},
},
},
});
const { queryByText } = within(main);
expect(queryByText("facebook")).toBeNull();
expect(queryByText("google")).toBeNull();
expect(queryByText("oidc")).toBeNull();
});
it("renders all social login disabled for stream target", async () => {
const { main } = await createTestRenderer({
Query: {
settings: {
auth: {
integrations: {
facebook: {
enabled: true,
targetFilter: {
stream: false,
},
},
google: {
enabled: true,
targetFilter: {
stream: false,
},
},
oidc: {
enabled: true,
targetFilter: {
stream: false,
},
},
},
},
},
},
});
const { queryByText } = within(main);
expect(queryByText("facebook")).toBeNull();
expect(queryByText("google")).toBeNull();
expect(queryByText("oidc")).toBeNull();
});
it("renders all social login disabled by turning off registration", async () => {
const { main } = await createTestRenderer({
Query: {
settings: {
auth: {
integrations: {
facebook: {
enabled: true,
allowRegistration: false,
},
google: {
enabled: true,
allowRegistration: false,
},
oidc: {
enabled: true,
allowRegistration: false,
},
},
},
},
},
});
const { queryByText } = within(main);
expect(queryByText("facebook")).toBeNull();
expect(queryByText("google")).toBeNull();
expect(queryByText("oidc")).toBeNull();
});
it("renders only some social login enabled", async () => {
const { main } = await createTestRenderer({
Query: {
settings: {
auth: {
integrations: {
local: {
enabled: false,
},
google: {
enabled: true,
},
facebook: {
enabled: true,
},
},
},
},
},
});
expect(toJSON(main)).toMatchSnapshot();
});
});
@@ -0,0 +1,103 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Form } from "react-final-form";
import { Bar, Title } from "talk-auth/components//Header";
import ConfirmEmailField from "talk-auth/components/ConfirmEmailField";
import EmailField from "talk-auth/components/EmailField";
import Main from "talk-auth/components/Main";
import AutoHeightContainer from "talk-auth/containers/AutoHeightContainer";
import { OnSubmit } from "talk-framework/lib/form";
import {
Button,
CallOut,
HorizontalGutter,
Icon,
Typography,
} from "talk-ui/components";
import { ListItem, UnorderedList } from "./UnorderedList";
interface FormProps {
email: string;
}
export interface AddEmailAddressForm {
onSubmit: OnSubmit<FormProps>;
}
const AddEmailAddress: StatelessComponent<AddEmailAddressForm> = props => {
return (
<div data-testid="addEmailAddress-container">
<Bar>
<Localized id="addEmailAddress-addEmailAddressHeader">
<Title>Add Email Address</Title>
</Localized>
</Bar>
<Main data-testid="addEmailAddress-main">
<Form onSubmit={props.onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<AutoHeightContainer />
<HorizontalGutter size="oneAndAHalf">
<Localized id="addEmailAddress-whatItIs">
<Typography variant="bodyCopy">
For your added security, we require users to add an email
address to their accounts. Your email address will be used
to:
</Typography>
</Localized>
<UnorderedList>
<ListItem icon={<Icon>done</Icon>}>
<Localized id="addEmailAddress-receiveUpdates">
<Typography container="div">
Receive updates regarding any changes to your account
(email address, username, password, etc.)
</Typography>
</Localized>
</ListItem>
<ListItem icon={<Icon>done</Icon>}>
<Localized id="addEmailAddress-allowDownload">
<Typography container="div">
Allow you to download your comments.
</Typography>
</Localized>
</ListItem>
<ListItem icon={<Icon>done</Icon>}>
<Localized id="addEmailAddress-sendNotifications">
<Typography container="div">
Send comment notifications that you have chosen to
receive.
</Typography>
</Localized>
</ListItem>
</UnorderedList>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<EmailField disabled={submitting} />
<ConfirmEmailField disabled={submitting} />
<Localized id="addEmailAddress-addEmailAddressButton">
<Button
variant="filled"
color="primary"
size="large"
type="submit"
fullWidth
disabled={submitting}
>
Add Email Address
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
</Main>
</div>
);
};
export default AddEmailAddress;
@@ -0,0 +1,10 @@
.root {
list-style-type: none;
display: flex;
}
.leftCol {
flex-grow: 0;
flex-shrink: 0;
width: 20px;
}
@@ -0,0 +1,23 @@
import React from "react";
import { StatelessComponent } from "react";
import { Icon } from "talk-ui/components";
import styles from "./ListItem.css";
interface Props {
icon?: React.ReactNode;
}
const ListItem: StatelessComponent<Props> = props => (
<li className={styles.root}>
<div className={styles.leftCol}>{props.icon}</div>
<div>{props.children}</div>
</li>
);
ListItem.defaultProps = {
icon: <Icon>keyboard_arrow_right</Icon>,
};
export default ListItem;
@@ -0,0 +1,11 @@
.root {
margin: 0;
padding: 0;
& > * {
margin: 0 0 calc(1 * var(--spacing-unit)) 0 !important;
}
& > *:last-child {
margin: 0 !important;
}
}
@@ -0,0 +1,16 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import ListItem from "./ListItem";
import UnorderedList from "./UnorderedList";
it("renders correctly", () => {
const renderer = createRenderer();
renderer.render(
<UnorderedList>
<ListItem icon={<span>-</span>}>1</ListItem>
<ListItem>2</ListItem>
</UnorderedList>
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,10 @@
import React from "react";
import { StatelessComponent } from "react";
import styles from "./UnorderedList.css";
const UnorderedList: StatelessComponent = props => (
<ul className={styles.root}>{props.children}</ul>
);
export default UnorderedList;
@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<ul
className="UnorderedList-root"
>
<ListItem
icon={
<span>
-
</span>
}
>
1
</ListItem>
<ListItem
icon={
<withPropsOnChange(Icon)>
keyboard_arrow_right
</withPropsOnChange(Icon)>
}
>
2
</ListItem>
</ul>
`;
@@ -0,0 +1,2 @@
export { default as UnorderedList } from "./UnorderedList";
export { default as ListItem } from "./ListItem";
@@ -0,0 +1,35 @@
import { FORM_ERROR } from "final-form";
import React, { Component } from "react";
import {
SetEmailMutation,
withSetEmailMutation,
} from "talk-auth/mutations/SetEmailMutation";
import { PropTypesOf } from "talk-framework/types";
import AddEmailAddress from "../components/AddEmailAddress";
interface Props {
setEmail: SetEmailMutation;
}
class AddEmailAddressContainer extends Component<Props> {
private handleSubmit: PropTypesOf<
typeof AddEmailAddress
>["onSubmit"] = async (input, form) => {
try {
await this.props.setEmail({ email: input.email });
return form.reset();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
};
public render() {
// tslint:disable-next-line:no-empty
return <AddEmailAddress onSubmit={this.handleSubmit} />;
}
}
const enhanced = withSetEmailMutation(AddEmailAddressContainer);
export default enhanced;
@@ -0,0 +1,73 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Form } from "react-final-form";
import { Bar, Title } from "talk-auth/components//Header";
import Main from "talk-auth/components/Main";
import AutoHeightContainer from "talk-auth/containers/AutoHeightContainer";
import { OnSubmit } from "talk-framework/lib/form";
import {
Button,
CallOut,
HorizontalGutter,
Typography,
} from "talk-ui/components";
import SetPasswordField from "talk-auth/components/SetPasswordField";
interface FormProps {
password: string;
}
export interface CreatePasswordForm {
onSubmit: OnSubmit<FormProps>;
}
const CreatePassword: StatelessComponent<CreatePasswordForm> = props => {
return (
<div data-testid="createPassword-container">
<Bar>
<Localized id="createPassword-createPasswordHeader">
<Title>Create Password</Title>
</Localized>
</Bar>
<Main data-testid="createPassword-main">
<Form onSubmit={props.onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<AutoHeightContainer />
<HorizontalGutter size="oneAndAHalf">
<Localized id="createPassword-whatItIs">
<Typography variant="bodyCopy">
To protect against unauthorized changes to your account, we
require users to create a password.
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<SetPasswordField disabled={submitting} />
<Localized id="createPassword-createPasswordButton">
<Button
variant="filled"
color="primary"
size="large"
type="submit"
fullWidth
disabled={submitting}
>
Create Password
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
</Main>
</div>
);
};
export default CreatePassword;
@@ -0,0 +1,36 @@
import { FORM_ERROR } from "final-form";
import React, { Component } from "react";
import {
SetPasswordMutation,
withSetPasswordMutation,
} from "talk-auth/mutations/SetPasswordMutation";
import { PropTypesOf } from "talk-framework/types";
import CreatePassword from "../components/CreatePassword";
interface Props {
setPassword: SetPasswordMutation;
}
class CreatePasswordContainer extends Component<Props> {
private handleSubmit: PropTypesOf<typeof CreatePassword>["onSubmit"] = async (
input,
form
) => {
try {
await this.props.setPassword({ password: input.password });
return form.reset();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
};
public render() {
// tslint:disable-next-line:no-empty
return <CreatePassword onSubmit={this.handleSubmit} />;
}
}
const enhanced = withSetPasswordMutation(CreatePasswordContainer);
export default enhanced;
@@ -0,0 +1,73 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Form } from "react-final-form";
import { Bar, Title } from "talk-auth/components//Header";
import Main from "talk-auth/components/Main";
import AutoHeightContainer from "talk-auth/containers/AutoHeightContainer";
import { OnSubmit } from "talk-framework/lib/form";
import {
Button,
CallOut,
HorizontalGutter,
Typography,
} from "talk-ui/components";
import UsernameField from "talk-auth/components/UsernameField";
interface FormProps {
username: string;
}
export interface CreateUsernameForm {
onSubmit: OnSubmit<FormProps>;
}
const CreateUsername: StatelessComponent<CreateUsernameForm> = props => {
return (
<div data-testid="createUsername-container">
<Bar>
<Localized id="createUsername-createUsernameHeader">
<Title>Create Username</Title>
</Localized>
</Bar>
<Main data-testid="createUsername-main">
<Form onSubmit={props.onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<AutoHeightContainer />
<HorizontalGutter size="oneAndAHalf">
<Localized id="createUsername-whatItIs">
<Typography variant="bodyCopy">
Your username is a unique identifier that will appear on all
of your comments.
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<UsernameField disabled={submitting} />
<Localized id="createUsername-createUsernameButton">
<Button
variant="filled"
color="primary"
size="large"
type="submit"
fullWidth
disabled={submitting}
>
Create Username
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
</Main>
</div>
);
};
export default CreateUsername;
@@ -0,0 +1,36 @@
import { FORM_ERROR } from "final-form";
import React, { Component } from "react";
import {
SetUsernameMutation,
withSetUsernameMutation,
} from "talk-auth/mutations/SetUsernameMutation";
import { PropTypesOf } from "talk-framework/types";
import CreateUsername from "../components/CreateUsername";
interface Props {
setUsername: SetUsernameMutation;
}
class CreateUsernameContainer extends Component<Props> {
private handleSubmit: PropTypesOf<typeof CreateUsername>["onSubmit"] = async (
input,
form
) => {
try {
await this.props.setUsername({ username: input.username });
return form.reset();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
};
public render() {
// tslint:disable-next-line:no-empty
return <CreateUsername onSubmit={this.handleSubmit} />;
}
}
const enhanced = withSetUsernameMutation(CreateUsernameContainer);
export default enhanced;
@@ -0,0 +1,114 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field, Form } from "react-final-form";
import { Bar, Title } from "talk-auth/components//Header";
import Main from "talk-auth/components/Main";
import AutoHeightContainer from "talk-auth/containers/AutoHeightContainer";
import { OnSubmit } from "talk-framework/lib/form";
import {
composeValidators,
required,
validateEmail,
} from "talk-framework/lib/validation";
import {
Button,
CallOut,
FormField,
HorizontalGutter,
InputLabel,
TextField,
Typography,
ValidationMessage,
} from "talk-ui/components";
interface FormProps {
email: string;
}
export interface ForgotPasswordForm {
onSubmit: OnSubmit<FormProps>;
}
const ForgotPassword: StatelessComponent<ForgotPasswordForm> = props => {
return (
<div data-testid="forgotPassword-container">
<Bar>
<Localized id="forgotPassword-forgotPasswordHeader">
<Title>Forgot Password</Title>
</Localized>
</Bar>
<Main data-testid="forgotPassword-main">
<Form onSubmit={props.onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<AutoHeightContainer />
<HorizontalGutter size="full">
<Localized id="forgotPassword-enterEmailAndGetALink">
<Typography variant="bodyCopy">
Enter your email address below and we will send you a link
to reset your password.
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<Field
name="email"
validate={composeValidators(required, validateEmail)}
>
{({ input, meta }) => (
<FormField>
<Localized id="forgotPassword-emailAddressLabel">
<InputLabel>Email Address</InputLabel>
</Localized>
<Localized
id="forgotPassword-emailAddressTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Email Address"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
fullWidth
disabled={submitting}
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Localized id="forgotPassword-sendEmailButton">
<Button
variant="filled"
color="primary"
size="large"
fullWidth
disabled={submitting}
>
Send Email
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
</Main>
</div>
);
};
export default ForgotPassword;

Some files were not shown because too many files have changed in this diff Show More