[next] Email (#2261)

* feat: suspending, banning, now propogation

* feat: added email rendering + localization support

* fix: fix related to lib

* refactor: moved juicer to queue task

* refactor: cleanup of job processor

* refactor: improved error messaging around failed email

* feat: initial forgot passwor impl

* fix: fixed rebase errors

* feat: send back Content-Language header with requests

* feat: added ban email

* feat: implemented forgotten password API

* fix: linting

* feat: support more emails

* fix: promise patches

* feat: initial confirm email API

* feat: added rate limiting

* feat: added URL support

* feat: added email docs

* fix: updated docs

* chore: documentation review

* fix: fixed build bug

* feat: implement forgot password in auth popup

* test: add tests + fixes

* chore: rename StatelessComponent to FunctionComponent

* fix: types and test fixes

* chore: upgrade deps

* fix: THANK YOU TESTS FOR SAVING MY A**

* chore: reorder imports

* chore: remove obsolete !

* feat: implement accounts bundle

* refactor: review suggestion

* fix: rebase upgrade error

* fix: rebase bug

* feat: reset password link support

* test: add tests for account password reset page

* fix: remove redirect uri

* fix: revert local state changes
This commit is contained in:
Wyatt Johnson
2019-05-09 22:54:56 +02:00
committed by Kiwi
parent 945bd7f2b0
commit df57b4eb17
487 changed files with 6794 additions and 2431 deletions
+4 -8
View File
@@ -1,12 +1,11 @@
import React, { StatelessComponent } from "react";
import React, { FunctionComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
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 ForgotPassword from "../views/forgotPassword/components/ForgotPassword";
import SignInContainer from "../views/signIn/containers/SignInContainer";
import SignUpContainer from "../views/signUp/containers/SignUpContainer";
import "./App.css";
@@ -15,7 +14,6 @@ export type View =
| "SIGN_UP"
| "SIGN_IN"
| "FORGOT_PASSWORD"
| "RESET_PASSWORD"
| "CREATE_USERNAME"
| "CREATE_PASSWORD"
| "ADD_EMAIL_ADDRESS"
@@ -34,9 +32,7 @@ const renderView = (view: AppProps["view"], auth: AppProps["auth"]) => {
case "SIGN_IN":
return <SignInContainer auth={auth} />;
case "FORGOT_PASSWORD":
return <ForgotPasswordContainer />;
case "RESET_PASSWORD":
return <ResetPasswordContainer />;
return <ForgotPassword />;
case "CREATE_USERNAME":
return <CreateUsernameContainer />;
case "CREATE_PASSWORD":
@@ -48,7 +44,7 @@ const renderView = (view: AppProps["view"], auth: AppProps["auth"]) => {
}
};
const App: StatelessComponent<AppProps> = ({ view, auth }) => (
const App: FunctionComponent<AppProps> = ({ view, auth }) => (
<div>{renderView(view, auth)}</div>
);
@@ -1,6 +1,6 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import { Field } from "react-final-form";
import {
composeValidators,
@@ -18,7 +18,7 @@ interface Props {
disabled: boolean;
}
const ConfirmEmailField: StatelessComponent<Props> = props => (
const ConfirmEmailField: FunctionComponent<Props> = props => (
<Field
name="confirmEmail"
validate={composeValidators(required, validateEqualEmails)}
@@ -1,6 +1,6 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import { Field } from "react-final-form";
import {
@@ -19,7 +19,7 @@ interface Props {
disabled: boolean;
}
const SetPasswordField: StatelessComponent<Props> = props => (
const SetPasswordField: FunctionComponent<Props> = props => (
<Field
name="confirmPassword"
validate={composeValidators(required, validateEqualPasswords)}
@@ -1,6 +1,6 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import { Field } from "react-final-form";
import {
composeValidators,
@@ -18,7 +18,7 @@ interface Props {
disabled: boolean;
}
const EmailField: StatelessComponent<Props> = props => (
const EmailField: FunctionComponent<Props> = props => (
<Field name="email" validate={composeValidators(required, validateEmail)}>
{({ input, meta }) => (
<FormField>
@@ -1,5 +1,5 @@
import React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import { Flex } from "talk-ui/components";
@@ -9,7 +9,7 @@ export interface BarProps {
children: React.ReactNode;
}
const Bar: StatelessComponent<BarProps> = props => (
const Bar: FunctionComponent<BarProps> = props => (
<Flex className={styles.root} alignItems="center" justifyContent="center">
<div>{props.children}</div>
</Flex>
@@ -1,5 +1,5 @@
import React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import { Flex } from "talk-ui/components";
@@ -9,7 +9,7 @@ export interface SubBarProps {
children: React.ReactNode;
}
const SubBar: StatelessComponent<SubBarProps> = props => (
const SubBar: FunctionComponent<SubBarProps> = props => (
<Flex className={styles.root} alignItems="center" justifyContent="center">
<div>{props.children}</div>
</Flex>
@@ -1,5 +1,5 @@
import React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import { Typography } from "talk-ui/components";
import styles from "./Subtitle.css";
@@ -8,7 +8,7 @@ interface Props {
children?: React.ReactNode;
}
const Subtitle: StatelessComponent<Props> = props => (
const Subtitle: FunctionComponent<Props> = props => (
<Typography variant="heading4" align="center" className={styles.root}>
{props.children}
</Typography>
@@ -1,5 +1,5 @@
import React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import { Typography } from "talk-ui/components";
import styles from "./Title.css";
@@ -8,7 +8,7 @@ interface Props {
children?: React.ReactNode;
}
const Title: StatelessComponent<Props> = props => (
const Title: FunctionComponent<Props> = props => (
<Typography variant="heading2" align="center" className={styles.root}>
{props.children}
</Typography>
@@ -1,5 +1,5 @@
import React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import { Flex } from "talk-ui/components";
@@ -9,7 +9,7 @@ interface Props {
children: string;
}
const HorizontalSeparator: StatelessComponent<Props> = props => (
const HorizontalSeparator: FunctionComponent<Props> = props => (
<Flex className={styles.root} alignItems="center" justifyContent="center">
<hr className={styles.hr} />
<div className={styles.text}>{props.children}</div>
+2 -2
View File
@@ -1,5 +1,5 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import React, { FunctionComponent } from "react";
import styles from "./Main.css";
@@ -8,7 +8,7 @@ export interface MainProps {
children: React.ReactNode;
}
const Main: StatelessComponent<MainProps> = ({
const Main: FunctionComponent<MainProps> = ({
children,
className,
...rest
@@ -1,10 +1,10 @@
import { Localized } from "fluent-react/compat";
import React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import HorizontalSeparator from "./HorizontalSeparator";
const OrSeparator: StatelessComponent = () => (
const OrSeparator: FunctionComponent = () => (
<Localized id="general-orSeparator">
<HorizontalSeparator>Or</HorizontalSeparator>
</Localized>
@@ -1,6 +1,6 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import { Field } from "react-final-form";
import { PasswordField } from "talk-framework/components";
import {
@@ -19,7 +19,7 @@ interface Props {
disabled: boolean;
}
const SetPasswordField: StatelessComponent<Props> = props => (
const SetPasswordField: FunctionComponent<Props> = props => (
<Field
name="password"
validate={composeValidators(required, validatePassword)}
@@ -1,6 +1,6 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import { Field } from "react-final-form";
import {
composeValidators,
@@ -19,7 +19,7 @@ interface Props {
disabled: boolean;
}
const CreateUsernameField: StatelessComponent<Props> = props => (
const CreateUsernameField: FunctionComponent<Props> = props => (
<Field
name="username"
validate={composeValidators(required, validateUsername)}
@@ -2,7 +2,7 @@
exports[`renders sign in 1`] = `
<div>
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(SignInContainer)))))))))
<withContext(withMutation(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(SignInContainer)))))))))
auth={Object {}}
/>
</div>
@@ -8,17 +8,18 @@ import {
CompleteAccountMutation,
SetViewMutation,
withCompleteAccountMutation,
withSetViewMutation,
} from "talk-auth/mutations";
import {
graphql,
MutationProp,
withFragmentContainer,
withLocalStateContainer,
withMutation,
} from "talk-framework/lib/relay";
interface Props {
completeAccount: CompleteAccountMutation;
setView: SetViewMutation;
setView: MutationProp<typeof SetViewMutation>;
local: Local;
auth: AuthData;
viewer: UserData | null;
@@ -119,7 +120,9 @@ const enhanced = withLocalStateContainer(
}
`,
})(
withSetViewMutation(withCompleteAccountMutation(AccountCompletionContainer))
withMutation(SetViewMutation)(
withCompleteAccountMutation(AccountCompletionContainer)
)
)
);
@@ -0,0 +1,26 @@
import { FunctionComponent, useEffect } from "react";
import { SetViewMutation } from "talk-auth/mutations";
import { View } from "talk-auth/mutations/SetViewMutation";
import { parseQuery } from "talk-common/utils";
import { useMutation } from "talk-framework/lib/relay";
/**
* ViewRouterContainer listens for changes to the location
* parses the `?view=` and calls the `setView` mutation.
*/
const ViewRouterContainer: FunctionComponent = () => {
const setView = useMutation(SetViewMutation);
useEffect(() => {
window.onpopstate = () => {
const query = parseQuery(window.location.search);
setView({ view: query.view as View });
};
return () => {
window.onpopstate = null;
};
}, [setView]);
return null;
};
export default ViewRouterContainer;
@@ -0,0 +1,6 @@
import { View } from "talk-auth/mutations/SetViewMutation";
import { modifyQuery } from "talk-framework/utils";
export default function getViewURL(view: View) {
return modifyQuery(window.location.href, { view });
}
+1
View File
@@ -0,0 +1 @@
export { default as getViewURL } from "./getViewURL";
+7 -3
View File
@@ -1,9 +1,10 @@
import React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import ReactDOM from "react-dom";
import { createManaged } from "talk-framework/lib/bootstrap";
import ViewRouterContainer from "./containers/ViewRouterContainer";
import resizePopup from "./dom/resizePopup";
import { initLocalState } from "./local";
import localesData from "./locales";
@@ -35,9 +36,12 @@ async function main() {
userLocales: navigator.languages,
});
const Index: StatelessComponent = () => (
const Index: FunctionComponent = () => (
<ManagedTalkContextProvider>
<AppQuery />
<>
<ViewRouterContainer />
<AppQuery />
</>
</ManagedTalkContextProvider>
);
-1
View File
@@ -2,7 +2,6 @@ enum View {
SIGN_UP
SIGN_IN
FORGOT_PASSWORD
RESET_PASSWORD
CREATE_USERNAME
CREATE_PASSWORD
ADD_EMAIL_ADDRESS
@@ -0,0 +1,12 @@
import { pick } from "lodash";
import { createMutation } from "talk-framework/lib/relay";
import { forgotPassword, ForgotPasswordInput } from "talk-framework/rest";
const ForgotPasswordMutation = createMutation(
"forgotPassword",
(_, input: ForgotPasswordInput, { rest }) =>
forgotPassword(rest, pick(input, ["email"]))
);
export default ForgotPasswordMutation;
@@ -3,7 +3,7 @@ import { Environment, RecordSource } from "relay-runtime";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import { commit } from "./SetViewMutation";
import SetViewMutation from "./SetViewMutation";
let environment: Environment;
const source: RecordSource = new RecordSource();
@@ -16,6 +16,6 @@ beforeAll(() => {
it("Sets view", () => {
const view = "SIGN_UP";
commit(environment, { view }, {} as any);
SetViewMutation.commit(environment, { view }, {} as any);
expect(source.get(LOCAL_ID)!.view).toEqual(view);
});
@@ -1,32 +1,56 @@
import { commitLocalUpdate, Environment } from "relay-runtime";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { createMutationContainer } from "talk-framework/lib/relay";
import { createMutation } from "talk-framework/lib/relay";
import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
export type View =
| "SIGN_IN"
| "SIGN_UP"
| "FORGOT_PASSWORD"
| "ADD_EMAIL_ADDRESS"
| "CREATE_USERNAME"
| "CREATE_PASSWORD";
export interface SetViewInput {
// TODO: replace with generated typescript types.
view:
| "SIGN_IN"
| "SIGN_UP"
| "FORGOT_PASSWORD"
| "RESET_PASSWORD"
| "ADD_EMAIL_ADDRESS"
| "CREATE_USERNAME"
| "CREATE_PASSWORD";
view: View;
history?: "push" | "replace";
}
export type SetViewMutation = (input: SetViewInput) => Promise<void>;
const SetViewMutation = createMutation(
"setView",
(environment: Environment, input: SetViewInput) => {
return commitLocalUpdate(environment, store => {
const record = store.get(LOCAL_ID)!;
export async function commit(
environment: Environment,
input: SetViewInput,
{ pym }: TalkContext
) {
return commitLocalUpdate(environment, store => {
const record = store.get(LOCAL_ID)!;
record.setValue(input.view, "view");
});
}
if (input.history) {
const newLocation = window.location.href.replace(
/\?[^#]*/,
`?view=${input.view}`
);
const previousState = window.history.state;
switch (input.history) {
case "push":
window.history.pushState(
previousState,
document.title,
newLocation
);
break;
case "replace":
window.history.replaceState(
previousState,
document.title,
newLocation
);
break;
default:
}
}
export const withSetViewMutation = createMutationContainer("setView", commit);
record.setValue(input.view, "view");
});
}
);
export default SetViewMutation;
+2 -1
View File
@@ -1,4 +1,4 @@
export { withSetViewMutation, SetViewMutation } from "./SetViewMutation";
export { default as SetViewMutation } from "./SetViewMutation";
export { withSignInMutation, SignInMutation } from "./SignInMutation";
export {
withCompleteAccountMutation,
@@ -18,3 +18,4 @@ export {
withSetPasswordMutation,
SetPasswordMutation,
} from "./SetPasswordMutation";
export { default as ForgotPasswordMutation } from "./ForgotPasswordMutation";
@@ -0,0 +1,94 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders forgot password view 1`] = `
<div>
<div
data-testid="forgotPassword-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"
>
Forgot Password?
</h1>
</div>
</div>
<div
className="Flex-root SubBar-root Flex-flex Flex-justifyCenter Flex-alignCenter"
>
<div>
<div
className="Flex-root Typography-root Typography-bodyCopy Typography-colorTextPrimary Flex-flex"
>
<a
className="TextLink-root"
href="http://localhost/?view=SIGN_IN"
onClick={[Function]}
>
Go back to sign in page
</a>
</div>
</div>
</div>
<div
className="Main-root"
data-testid="forgotPassword-main"
>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<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"
htmlFor="email"
>
Email Address
</label>
<div
className="TextField-root TextField-fullWidth"
>
<input
className="TextField-input TextField-colorRegular"
disabled={false}
id="email"
name="email"
onChange={[Function]}
placeholder="Email Address"
type="text"
value=""
/>
</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"
>
Send Email
</button>
</div>
</form>
</div>
</div>
</div>
`;
@@ -89,20 +89,17 @@ exports[`accepts correct password 1`] = `
<div
className="Flex-root Flex-flex Flex-justifyFlexEnd"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
data-testid="gotoForgotPasswordButton"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Forgot your password?
</button>
<a
className="TextLink-root"
href="http://localhost/?view=FORGOT_PASSWORD"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
@@ -218,20 +215,17 @@ exports[`accepts valid email 1`] = `
<div
className="Flex-root Flex-flex Flex-justifyFlexEnd"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
data-testid="gotoForgotPasswordButton"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Forgot your password?
</button>
<a
className="TextLink-root"
href="http://localhost/?view=FORGOT_PASSWORD"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
@@ -341,20 +335,17 @@ exports[`auth configuration renders all auth enabled 1`] = `
<div
className="Flex-root Flex-flex Flex-justifyFlexEnd"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
data-testid="gotoForgotPasswordButton"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Forgot your password?
</button>
<a
className="TextLink-root"
href="http://localhost/?view=FORGOT_PASSWORD#accessToken=access-token"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
@@ -635,20 +626,17 @@ exports[`checks for invalid email 1`] = `
<div
className="Flex-root Flex-flex Flex-justifyFlexEnd"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
data-testid="gotoForgotPasswordButton"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Forgot your password?
</button>
<a
className="TextLink-root"
href="http://localhost/?view=FORGOT_PASSWORD"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
@@ -706,19 +694,13 @@ exports[`renders sign in view 1`] = `
<span>
Don't have an account? 
</span>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
data-testid="gotoSignUpButton"
onBlur={[Function]}
<a
className="TextLink-root"
href="http://localhost/?view=SIGN_UP"
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign Up
</button>
</a>
</div>
</div>
</div>
@@ -804,20 +786,17 @@ exports[`renders sign in view 1`] = `
<div
className="Flex-root Flex-flex Flex-justifyFlexEnd"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
data-testid="gotoForgotPasswordButton"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Forgot your password?
</button>
<a
className="TextLink-root"
href="http://localhost/?view=FORGOT_PASSWORD"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
@@ -881,19 +860,13 @@ exports[`renders sign in view with error 1`] = `
<span>
Don't have an account? 
</span>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
data-testid="gotoSignUpButton"
onBlur={[Function]}
<a
className="TextLink-root"
href="http://localhost/?view=SIGN_UP"
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign Up
</button>
</a>
</div>
</div>
</div>
@@ -984,20 +957,17 @@ exports[`renders sign in view with error 1`] = `
<div
className="Flex-root Flex-flex Flex-justifyFlexEnd"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
data-testid="gotoForgotPasswordButton"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Forgot your password?
</button>
<a
className="TextLink-root"
href="http://localhost/?view=FORGOT_PASSWORD"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
@@ -1132,20 +1102,17 @@ exports[`shows error when submitting empty form 1`] = `
<div
className="Flex-root Flex-flex Flex-justifyFlexEnd"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
data-testid="gotoForgotPasswordButton"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Forgot your password?
</button>
<a
className="TextLink-root"
href="http://localhost/?view=FORGOT_PASSWORD"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
@@ -1253,20 +1220,17 @@ exports[`shows server error 1`] = `
<div
className="Flex-root Flex-flex Flex-justifyFlexEnd"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
data-testid="gotoForgotPasswordButton"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Forgot your password?
</button>
<a
className="TextLink-root"
href="http://localhost/?view=FORGOT_PASSWORD"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
@@ -1369,20 +1333,17 @@ exports[`submits form successfully 1`] = `
<div
className="Flex-root Flex-flex Flex-justifyFlexEnd"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
data-testid="gotoForgotPasswordButton"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Forgot your password?
</button>
<a
className="TextLink-root"
href="http://localhost/?view=FORGOT_PASSWORD"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
@@ -1747,19 +1747,13 @@ exports[`renders sign up form 1`] = `
<span>
Already have an account? 
</span>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
data-testid="gotoSignInButton"
onBlur={[Function]}
<a
className="TextLink-root"
href="http://localhost/?view=SIGN_IN"
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign In
</button>
</a>
</div>
</div>
</div>
@@ -0,0 +1,152 @@
import { act } from "react-test-renderer";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import { GQLResolver } from "talk-framework/schema";
import {
createResolversStub,
CreateTestRendererParams,
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(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue("FORGOT_PASSWORD", "view");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("forgotPassword-container")
);
const main = within(testRenderer.root).getByTestID(/.*-main/);
const form = within(main).queryByType("form")!;
const emailField = within(form).getByLabelText("Email Address");
return {
context,
testRenderer,
form,
main,
container,
emailField,
};
}
beforeEach(async () => {
windowMock = mockWindow();
});
afterEach(async () => {
await wait(() => expect(windowMock.resizeStub.called).toBe(true));
windowMock.restore();
});
it("renders forgot password view", async () => {
const { testRenderer } = await createTestRenderer();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("shows error when submitting empty form", async () => {
const { form } = await createTestRenderer();
act(() => {
form!.props.onSubmit();
});
within(form).getByText("This field is required", { exact: false });
});
it("checks for invalid email", async () => {
const { form, emailField } = await createTestRenderer();
act(() => {
emailField.props.onChange("invalidemail");
form!.props.onSubmit();
});
within(form).getByText("Please enter a valid email address", {
exact: false,
});
});
it("shows server error", async () => {
const { form, context, emailField } = await createTestRenderer();
const error = new Error("Server Error");
const restMock = sinon.mock(context.rest);
restMock
.expects("fetch")
.withArgs("/auth/local/forgot", {
method: "POST",
body: {
email: "hans@test.com",
},
})
.once()
.throws(error);
act(() => {
emailField.props.onChange("hans@test.com");
form!.props.onSubmit();
});
await waitForElement(() =>
within(form).getByText("Server Error", {
exact: false,
})
);
restMock.verify();
});
it("submits form successfully", async () => {
const {
form,
context,
emailField,
testRenderer,
} = await createTestRenderer();
const restMock = sinon.mock(context.rest);
restMock
.expects("fetch")
.withArgs("/auth/local/forgot", {
method: "POST",
body: {
email: "hans@test.com",
},
})
.once();
act(() => {
emailField.props.onChange("hans@test.com");
form!.props.onSubmit();
});
await waitForElement(() =>
within(testRenderer.root).getByText("Check Your Email", { exact: false })
);
within(testRenderer.root)
.getByText("Close")
.props.onClick();
restMock.verify();
expect(windowMock.closeStub.called).toBe(true);
});
+19 -6
View File
@@ -49,8 +49,8 @@ it("navigates to sign up form", async () => {
within(testRenderer.root).getByTestID("signIn-container")
);
within(container)
.getByTestID("gotoSignUpButton")
.props.onClick();
.getByText("Sign Up")
.props.onClick({});
await waitForElement(() =>
within(testRenderer.root).getByTestID("signUp-container")
);
@@ -62,8 +62,8 @@ it("navigates to sign in form", async () => {
within(testRenderer.root).getByTestID("signUp-container")
);
within(container)
.getByTestID("gotoSignInButton")
.props.onClick();
.getByText("Sign In")
.props.onClick({});
await waitForElement(() =>
within(testRenderer.root).getByTestID("signIn-container")
);
@@ -75,9 +75,22 @@ it("navigates to forgot password form", async () => {
within(testRenderer.root).getByTestID("signIn-container")
);
within(container)
.getByTestID("gotoForgotPasswordButton")
.props.onClick();
.getByText("Forgot your password?")
.props.onClick({});
await waitForElement(() =>
within(testRenderer.root).getByTestID("forgotPassword-container")
);
});
it("navigates back from forgot password form", async () => {
const testRenderer = await createTestRenderer("FORGOT_PASSWORD");
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("forgotPassword-container")
);
within(container)
.getByText("back to sign in", { exact: false })
.props.onClick({});
await waitForElement(() =>
within(testRenderer.root).getByTestID("signIn-container")
);
});
+4 -4
View File
@@ -74,11 +74,11 @@ it("renders sign in view with error", async () => {
);
expect(within(container).toJSON()).toMatchSnapshot();
within(testRenderer.root)
.getByTestID("gotoSignUpButton")
.props.onClick();
.getByText("Sign Up")
.props.onClick({});
within(testRenderer.root)
.getByTestID("gotoSignInButton")
.props.onClick();
.getByText("Sign In")
.props.onClick({});
const container2 = await waitForElement(() =>
within(testRenderer.root).getByTestID("signIn-container")
);
@@ -1,5 +1,5 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import React, { FunctionComponent } from "react";
import { Form } from "react-final-form";
import { Bar, Title } from "talk-auth/components//Header";
@@ -26,7 +26,7 @@ export interface AddEmailAddressForm {
onSubmit: OnSubmit<FormProps>;
}
const AddEmailAddress: StatelessComponent<AddEmailAddressForm> = props => {
const AddEmailAddress: FunctionComponent<AddEmailAddressForm> = props => {
return (
<div data-testid="addEmailAddress-container">
<Bar>
@@ -1,5 +1,5 @@
import React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import { Icon } from "talk-ui/components";
@@ -9,7 +9,7 @@ interface Props {
icon?: React.ReactNode;
}
const ListItem: StatelessComponent<Props> = props => (
const ListItem: FunctionComponent<Props> = props => (
<li className={styles.root}>
<div className={styles.leftCol}>{props.icon}</div>
<div>{props.children}</div>
@@ -1,9 +1,9 @@
import React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import styles from "./UnorderedList.css";
const UnorderedList: StatelessComponent = props => (
const UnorderedList: FunctionComponent = props => (
<ul className={styles.root}>{props.children}</ul>
);
@@ -1,5 +1,5 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import React, { FunctionComponent } from "react";
import { Form } from "react-final-form";
import { Bar, Title } from "talk-auth/components//Header";
@@ -23,7 +23,7 @@ export interface CreatePasswordForm {
onSubmit: OnSubmit<FormProps>;
}
const CreatePassword: StatelessComponent<CreatePasswordForm> = props => {
const CreatePassword: FunctionComponent<CreatePasswordForm> = props => {
return (
<div data-testid="createPassword-container">
<Bar>
@@ -1,5 +1,5 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import React, { FunctionComponent } from "react";
import { Form } from "react-final-form";
import { Bar, Title } from "talk-auth/components//Header";
@@ -23,7 +23,7 @@ export interface CreateUsernameForm {
onSubmit: OnSubmit<FormProps>;
}
const CreateUsername: StatelessComponent<CreateUsernameForm> = props => {
const CreateUsername: FunctionComponent<CreateUsernameForm> = props => {
return (
<div data-testid="createUsername-container">
<Bar>
@@ -0,0 +1,54 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent, useCallback } from "react";
import { Bar, Title } from "talk-auth/components/Header";
import Main from "talk-auth/components/Main";
import { Button, HorizontalGutter, Typography } from "talk-ui/components";
interface Props {
email: string;
}
const CheckEmail: FunctionComponent<Props> = ({ email }) => {
const closeWindow = useCallback(() => {
window.close();
}, []);
const UserEmail = () => <strong>{email}</strong>;
return (
<div data-testid="forgotPassword-checkEmail-container">
<Bar>
<Localized id="forgotPassword-checkEmail-checkEmailHeader">
<Title>Check Your Email</Title>
</Localized>
</Bar>
<Main data-testid="forgotPassword-checkEmail-main">
<HorizontalGutter size="full">
<Localized
id="forgotPassword-checkEmail-receiveEmail"
email={<UserEmail />}
>
<Typography variant="bodyCopy">
{
"If there is an account associated with <email></email>, you will receive an email with a link to create a new password."
}
</Typography>
</Localized>
<Localized id="forgotPassword-checkEmail-closeButton">
<Button
variant="filled"
color="primary"
size="large"
fullWidth
type="submit"
onClick={closeWindow}
>
Close
</Button>
</Localized>
</HorizontalGutter>
</Main>
</div>
);
};
export default CheckEmail;
@@ -1,112 +1,14 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field, Form } from "react-final-form";
import React, { FunctionComponent, useState } from "react";
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";
import CheckEmail from "./CheckEmail";
import ForgotPasswordForm from "./ForgotPasswordForm";
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>
const ForgotPassword: FunctionComponent = () => {
const [checkEmail, setCheckEmail] = useState<string | null>(null);
return checkEmail ? (
<CheckEmail email={checkEmail} />
) : (
<ForgotPasswordForm onCheckEmail={setCheckEmail} />
);
};
@@ -0,0 +1,162 @@
import { FORM_ERROR } from "final-form";
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent, useCallback } from "react";
import { Field, Form } from "react-final-form";
import { Bar, SubBar, Title } from "talk-auth/components/Header";
import Main from "talk-auth/components/Main";
import AutoHeightContainer from "talk-auth/containers/AutoHeightContainer";
import { getViewURL } from "talk-auth/helpers";
import { ForgotPasswordMutation, SetViewMutation } from "talk-auth/mutations";
import { InvalidRequestError } from "talk-framework/lib/errors";
import { useMutation } from "talk-framework/lib/relay";
import {
composeValidators,
required,
validateEmail,
} from "talk-framework/lib/validation";
import {
Button,
CallOut,
Flex,
FormField,
HorizontalGutter,
InputLabel,
TextField,
TextLink,
Typography,
ValidationMessage,
} from "talk-ui/components";
interface FormProps {
email: string;
}
interface Props {
onCheckEmail: (email: string) => void;
}
const ForgotPasswordForm: FunctionComponent<Props> = ({ onCheckEmail }) => {
const signInHref = getViewURL("SIGN_IN");
const forgotPassword = useMutation(ForgotPasswordMutation);
const setView = useMutation(SetViewMutation);
const onSubmit = useCallback(
async ({ email }: FormProps) => {
try {
await forgotPassword({
email,
});
onCheckEmail(email);
} catch (error) {
if (error instanceof InvalidRequestError) {
return error.invalidArgs;
}
return { [FORM_ERROR]: error.message };
}
return;
},
[forgotPassword]
);
const onGotoSignIn = useCallback(
(e: React.MouseEvent) => {
setView({ view: "SIGN_IN", history: "push" });
if (e.preventDefault) {
e.preventDefault();
}
},
[setView]
);
return (
<div data-testid="forgotPassword-container">
<Bar>
<Localized id="forgotPassword-forgotPasswordHeader">
<Title>Forgot Password?</Title>
</Localized>
</Bar>
<SubBar>
<Typography variant="bodyCopy" container={Flex}>
<Localized id="forgotPassword-gotBackToSignIn">
<TextLink href={signInHref} onClick={onGotoSignIn}>
Go back to sign in page
</TextLink>
</Localized>
</Typography>
</SubBar>
<Main data-testid="forgotPassword-main">
<Form onSubmit={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 htmlFor={input.name}>
Email Address
</InputLabel>
</Localized>
<Localized
id="forgotPassword-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"
}
fullWidth
disabled={submitting}
/>
</Localized>
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Localized id="forgotPassword-sendEmailButton">
<Button
variant="filled"
color="primary"
size="large"
fullWidth
type="submit"
disabled={submitting}
>
Send Email
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
</Main>
</div>
);
};
export default ForgotPasswordForm;
@@ -1,11 +0,0 @@
import React, { Component } from "react";
import ForgotPassword from "../components/ForgotPassword";
class ForgotPasswordContainer extends Component {
public render() {
// tslint:disable-next-line:no-empty
return <ForgotPassword onSubmit={() => {}} />;
}
}
export default ForgotPasswordContainer;
@@ -1,63 +0,0 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Form } from "react-final-form";
import { OnSubmit } from "talk-framework/lib/form";
import { Bar, Title } from "talk-auth/components//Header";
import ConfirmPasswordField from "talk-auth/components/ConfirmPasswordField";
import Main from "talk-auth/components/Main";
import SetPasswordField from "talk-auth/components/SetPasswordField";
import AutoHeightContainer from "talk-auth/containers/AutoHeightContainer";
import { Button, CallOut, HorizontalGutter } from "talk-ui/components";
interface FormProps {
password: string;
confirmPassword: string;
}
export interface ResetPasswordForm {
onSubmit: OnSubmit<FormProps>;
}
const ResetPassword: StatelessComponent<ResetPasswordForm> = props => {
return (
<div data-testid="resetPassword-container">
<Bar>
<Localized id="resetPassword-resetPasswordHeader">
<Title>Reset Password</Title>
</Localized>
</Bar>
<Main data-testid="resetPassword-main">
<Form onSubmit={props.onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<AutoHeightContainer />
<HorizontalGutter size="full">
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<SetPasswordField disabled={submitting} />
<ConfirmPasswordField disabled={submitting} />
<Localized id="resetPassword-resetPasswordButton">
<Button
variant="filled"
color="primary"
size="large"
fullWidth
disabled={submitting}
>
Reset Password
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
</Main>
</div>
);
};
export default ResetPassword;
@@ -1,11 +0,0 @@
import React, { Component } from "react";
import ResetPassword from "../components/ResetPassword";
class ResetPasswordContainer extends Component {
public render() {
// tslint:disable-next-line:no-empty
return <ResetPassword onSubmit={() => {}} />;
}
}
export default ResetPasswordContainer;
@@ -12,6 +12,7 @@ const SignInN = removeFragmentRefs(SignIn);
it("renders correctly", () => {
const props: PropTypesOf<typeof SignInN> = {
onGotoSignUp: noop,
signUpHref: "/signUp",
emailEnabled: true,
facebookEnabled: true,
googleEnabled: true,
@@ -27,6 +28,7 @@ it("renders correctly", () => {
it("renders error", () => {
const props: PropTypesOf<typeof SignInN> = {
onGotoSignUp: noop,
signUpHref: "/signUp",
emailEnabled: true,
facebookEnabled: true,
googleEnabled: true,
@@ -42,6 +44,7 @@ it("renders error", () => {
it("renders without email login", () => {
const props: PropTypesOf<typeof SignInN> = {
onGotoSignUp: noop,
signUpHref: "/signUp",
emailEnabled: false,
facebookEnabled: true,
googleEnabled: true,
@@ -1,5 +1,5 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import React, { FunctionComponent } from "react";
import { Bar, SubBar, Subtitle, Title } from "talk-auth/components//Header";
import Main from "talk-auth/components/Main";
@@ -7,10 +7,10 @@ import OrSeparator from "talk-auth/components/OrSeparator";
import AutoHeightContainer from "talk-auth/containers/AutoHeightContainer";
import { PropTypesOf } from "talk-framework/types";
import {
Button,
CallOut,
Flex,
HorizontalGutter,
TextLink,
Typography,
} from "talk-ui/components";
@@ -21,7 +21,8 @@ import SignInWithOIDCContainer from "../containers/SignInWithOIDCContainer";
export interface SignInForm {
error: string | null;
onGotoSignUp: () => void;
onGotoSignUp: React.EventHandler<React.MouseEvent>;
signUpHref: string;
emailEnabled?: boolean;
facebookEnabled?: boolean;
googleEnabled?: boolean;
@@ -31,12 +32,13 @@ export interface SignInForm {
PropTypesOf<typeof SignInWithGoogleContainer>["auth"];
}
const SignIn: StatelessComponent<SignInForm> = ({
const SignIn: FunctionComponent<SignInForm> = ({
onGotoSignUp,
emailEnabled,
facebookEnabled,
googleEnabled,
oidcEnabled,
signUpHref,
auth,
error,
}) => {
@@ -59,18 +61,10 @@ const SignIn: StatelessComponent<SignInForm> = ({
<SubBar>
<Localized
id="signIn-noAccountSignUp"
button={
<Button
data-testid="gotoSignUpButton"
variant="underlined"
size="small"
color="primary"
onClick={onGotoSignUp}
/>
}
textlink={<TextLink onClick={onGotoSignUp} href={signUpHref} />}
>
<Typography variant="bodyCopy" container={Flex}>
{"Don't have an account? <button>Sign Up</button>"}
{"Don't have an account? <textlink>Sign Up</textlink>"}
</Typography>
</Localized>
</SubBar>
@@ -1,5 +1,5 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import React, { FunctionComponent } from "react";
import { Field, Form } from "react-final-form";
import { OnSubmit } from "talk-framework/lib/form";
@@ -15,6 +15,8 @@ import {
FormField,
HorizontalGutter,
InputLabel,
TextLink,
Typography,
ValidationMessage,
} from "talk-ui/components";
@@ -25,10 +27,11 @@ interface FormProps {
export interface SignInWithEmailForm {
onSubmit: OnSubmit<FormProps>;
onGotoForgotPassword: () => void;
onGotoForgotPassword: React.EventHandler<React.MouseEvent>;
forgotPasswordHref: string;
}
const SignInWithEmail: StatelessComponent<SignInWithEmailForm> = props => {
const SignInWithEmail: FunctionComponent<SignInWithEmailForm> = props => {
return (
<Form onSubmit={props.onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
@@ -73,18 +76,16 @@ const SignInWithEmail: StatelessComponent<SignInWithEmailForm> = props => {
</ValidationMessage>
)}
<Flex justifyContent="flex-end">
<Localized id="signIn-forgotYourPassword">
<Button
data-testid="gotoForgotPasswordButton"
variant="underlined"
color="primary"
size="small"
disabled={submitting}
onClick={props.onGotoForgotPassword}
>
Forgot your password?
</Button>
</Localized>
<Typography variant="bodyCopy">
<Localized id="signIn-forgotYourPassword">
<TextLink
onClick={props.onGotoForgotPassword}
href={props.forgotPasswordHref}
>
Forgot your password?
</TextLink>
</Localized>
</Typography>
</Flex>
</FormField>
)}
@@ -16,16 +16,13 @@ exports[`renders correctly 1`] = `
</Localized>
<SubBar>
<Localized
button={
<ForwardRef(forwardRef)
color="primary"
data-testid="gotoSignUpButton"
id="signIn-noAccountSignUp"
textlink={
<withPropsOnChange(TextLinkProps)
href="/signUp"
onClick={[Function]}
size="small"
variant="underlined"
/>
}
id="signIn-noAccountSignUp"
>
<ForwardRef(forwardRef)
container={
@@ -36,7 +33,7 @@ exports[`renders correctly 1`] = `
}
variant="bodyCopy"
>
Don't have an account? &lt;button&gt;Sign Up&lt;/button&gt;
Don't have an account? &lt;textlink&gt;Sign Up&lt;/textlink&gt;
</ForwardRef(forwardRef)>
</Localized>
</SubBar>
@@ -46,7 +43,7 @@ exports[`renders correctly 1`] = `
<ForwardRef(forwardRef)
size="oneAndAHalf"
>
<withContext(createMutationContainer(withContext(createMutationContainer(SignInContainer)))) />
<withContext(withMutation(withContext(createMutationContainer(SignInContainer)))) />
<OrSeparator />
<ForwardRef(forwardRef)>
<Relay(SignInWithFacebookContainer)
@@ -80,16 +77,13 @@ exports[`renders error 1`] = `
</Localized>
<SubBar>
<Localized
button={
<ForwardRef(forwardRef)
color="primary"
data-testid="gotoSignUpButton"
id="signIn-noAccountSignUp"
textlink={
<withPropsOnChange(TextLinkProps)
href="/signUp"
onClick={[Function]}
size="small"
variant="underlined"
/>
}
id="signIn-noAccountSignUp"
>
<ForwardRef(forwardRef)
container={
@@ -100,7 +94,7 @@ exports[`renders error 1`] = `
}
variant="bodyCopy"
>
Don't have an account? &lt;button&gt;Sign Up&lt;/button&gt;
Don't have an account? &lt;textlink&gt;Sign Up&lt;/textlink&gt;
</ForwardRef(forwardRef)>
</Localized>
</SubBar>
@@ -116,7 +110,7 @@ exports[`renders error 1`] = `
>
Server Error
</withPropsOnChange(CallOut)>
<withContext(createMutationContainer(withContext(createMutationContainer(SignInContainer)))) />
<withContext(withMutation(withContext(createMutationContainer(SignInContainer)))) />
<OrSeparator />
<ForwardRef(forwardRef)>
<Relay(SignInWithFacebookContainer)
@@ -150,16 +144,13 @@ exports[`renders without email login 1`] = `
</Localized>
<SubBar>
<Localized
button={
<ForwardRef(forwardRef)
color="primary"
data-testid="gotoSignUpButton"
id="signIn-noAccountSignUp"
textlink={
<withPropsOnChange(TextLinkProps)
href="/signUp"
onClick={[Function]}
size="small"
variant="underlined"
/>
}
id="signIn-noAccountSignUp"
>
<ForwardRef(forwardRef)
container={
@@ -170,7 +161,7 @@ exports[`renders without email login 1`] = `
}
variant="bodyCopy"
>
Don't have an account? &lt;button&gt;Sign Up&lt;/button&gt;
Don't have an account? &lt;textlink&gt;Sign Up&lt;/textlink&gt;
</ForwardRef(forwardRef)>
</Localized>
</SubBar>
@@ -7,27 +7,34 @@ import {
SetViewMutation,
SignInMutation,
withClearErrorMutation,
withSetViewMutation,
withSignInMutation,
} from "talk-auth/mutations";
import {
graphql,
MutationProp,
withFragmentContainer,
withLocalStateContainer,
withMutation,
} from "talk-framework/lib/relay";
import { getViewURL } from "talk-auth/helpers";
import SignIn from "../components/SignIn";
interface Props {
local: LocalData;
auth: AuthData;
signIn: SignInMutation;
setView: SetViewMutation;
setView: MutationProp<typeof SetViewMutation>;
clearError: ClearErrorMutation;
}
class SignInContainer extends Component<Props> {
private goToSignUp = () => this.props.setView({ view: "SIGN_UP" });
private goToSignUp = (e: React.MouseEvent) => {
this.props.setView({ view: "SIGN_UP", history: "push" });
if (e.preventDefault) {
e.preventDefault();
}
};
public componentWillUnmount() {
this.props.clearError();
@@ -53,12 +60,13 @@ class SignInContainer extends Component<Props> {
oidcEnabled={
integrations.oidc.enabled && integrations.oidc.targetFilter.stream
}
signUpHref={getViewURL("SIGN_UP")}
/>
);
}
}
const enhanced = withSetViewMutation(
const enhanced = withMutation(SetViewMutation)(
withClearErrorMutation(
withSignInMutation(
withLocalStateContainer(
@@ -4,17 +4,18 @@ import React, { Component } from "react";
import {
SetViewMutation,
SignInMutation,
withSetViewMutation,
withSignInMutation,
} from "talk-auth/mutations";
import { MutationProp, withMutation } from "talk-framework/lib/relay";
import { getViewURL } from "talk-auth/helpers";
import SignInWithEmail, {
SignInWithEmailForm,
} from "../components/SignInWithEmail";
interface SignInContainerProps {
signIn: SignInMutation;
setView: SetViewMutation;
setView: MutationProp<typeof SetViewMutation>;
}
class SignInContainer extends Component<SignInContainerProps> {
@@ -26,17 +27,24 @@ class SignInContainer extends Component<SignInContainerProps> {
return { [FORM_ERROR]: error.message };
}
};
private goToForgotPassword = () =>
this.props.setView({ view: "FORGOT_PASSWORD" });
private goToForgotPassword = (e: React.MouseEvent) => {
this.props.setView({ view: "FORGOT_PASSWORD", history: "push" });
if (e.preventDefault) {
e.preventDefault();
}
};
public render() {
return (
<SignInWithEmail
onSubmit={this.onSubmit}
onGotoForgotPassword={this.goToForgotPassword}
forgotPasswordHref={getViewURL("FORGOT_PASSWORD")}
/>
);
}
}
const enhanced = withSetViewMutation(withSignInMutation(SignInContainer));
const enhanced = withMutation(SetViewMutation)(
withSignInMutation(SignInContainer)
);
export default enhanced;
@@ -17,6 +17,7 @@ it("renders correctly", () => {
googleEnabled: true,
oidcEnabled: true,
auth: {},
signInHref: "/signIn",
};
const renderer = createRenderer();
renderer.render(<SignUpN {...props} />);
@@ -31,6 +32,7 @@ it("renders without email sign up", () => {
googleEnabled: true,
oidcEnabled: true,
auth: {},
signInHref: "/signIn",
};
const renderer = createRenderer();
renderer.render(<SignUpN {...props} />);
@@ -1,12 +1,17 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import React, { FunctionComponent } from "react";
import { Bar, SubBar, Subtitle, Title } from "talk-auth/components//Header";
import Main from "talk-auth/components/Main";
import OrSeparator from "talk-auth/components/OrSeparator";
import AutoHeightContainer from "talk-auth/containers/AutoHeightContainer";
import { PropTypesOf } from "talk-framework/types";
import { Button, Flex, HorizontalGutter, Typography } from "talk-ui/components";
import {
Flex,
HorizontalGutter,
TextLink,
Typography,
} from "talk-ui/components";
import SignUpWithEmailContainer from "../containers/SignUpWithEmailContainer";
import SignUpWithFacebookContainer from "../containers/SignUpWithFacebookContainer";
@@ -14,7 +19,8 @@ import SignUpWithGoogleContainer from "../containers/SignUpWithGoogleContainer";
import SignUpWithOIDCContainer from "../containers/SignUpWithOIDCContainer";
interface Props {
onGotoSignIn: () => void;
onGotoSignIn: React.EventHandler<React.MouseEvent>;
signInHref: string;
emailEnabled?: boolean;
facebookEnabled?: boolean;
googleEnabled?: boolean;
@@ -24,12 +30,13 @@ interface Props {
PropTypesOf<typeof SignUpWithGoogleContainer>["auth"];
}
const SignUp: StatelessComponent<Props> = ({
const SignUp: FunctionComponent<Props> = ({
onGotoSignIn,
emailEnabled,
facebookEnabled,
googleEnabled,
oidcEnabled,
signInHref,
auth,
}) => {
const oneClickUptegrationEnabled =
@@ -51,18 +58,10 @@ const SignUp: StatelessComponent<Props> = ({
<SubBar>
<Localized
id="signUp-accountAvailableSignIn"
button={
<Button
data-testid="gotoSignInButton"
variant="underlined"
size="small"
color="primary"
onClick={onGotoSignIn}
/>
}
textlink={<TextLink onClick={onGotoSignIn} href={signInHref} />}
>
<Typography variant="bodyCopy" container={Flex}>
{"Already have an account? <button>Sign In</button>"}
{"Already have an account? <textlink>Sign In</textlink>"}
</Typography>
</Localized>
</SubBar>
@@ -1,6 +1,6 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { FunctionComponent } from "react";
import { Form } from "react-final-form";
import EmailField from "talk-auth/components/EmailField";
@@ -27,7 +27,7 @@ interface Props {
onSubmit: OnSubmit<FormProps>;
}
const SignUp: StatelessComponent<Props> = props => {
const SignUp: FunctionComponent<Props> = props => {
return (
<Form onSubmit={props.onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
@@ -16,16 +16,13 @@ exports[`renders correctly 1`] = `
</Localized>
<SubBar>
<Localized
button={
<ForwardRef(forwardRef)
color="primary"
data-testid="gotoSignInButton"
id="signUp-accountAvailableSignIn"
textlink={
<withPropsOnChange(TextLinkProps)
href="/signIn"
onClick={[Function]}
size="small"
variant="underlined"
/>
}
id="signUp-accountAvailableSignIn"
>
<ForwardRef(forwardRef)
container={
@@ -36,7 +33,7 @@ exports[`renders correctly 1`] = `
}
variant="bodyCopy"
>
Already have an account? &lt;button&gt;Sign In&lt;/button&gt;
Already have an account? &lt;textlink&gt;Sign In&lt;/textlink&gt;
</ForwardRef(forwardRef)>
</Localized>
</SubBar>
@@ -80,16 +77,13 @@ exports[`renders without email sign up 1`] = `
</Localized>
<SubBar>
<Localized
button={
<ForwardRef(forwardRef)
color="primary"
data-testid="gotoSignInButton"
id="signUp-accountAvailableSignIn"
textlink={
<withPropsOnChange(TextLinkProps)
href="/signIn"
onClick={[Function]}
size="small"
variant="underlined"
/>
}
id="signUp-accountAvailableSignIn"
>
<ForwardRef(forwardRef)
container={
@@ -100,7 +94,7 @@ exports[`renders without email sign up 1`] = `
}
variant="bodyCopy"
>
Already have an account? &lt;button&gt;Sign In&lt;/button&gt;
Already have an account? &lt;textlink&gt;Sign In&lt;/textlink&gt;
</ForwardRef(forwardRef)>
</Localized>
</SubBar>
@@ -1,22 +1,34 @@
import React, { Component } from "react";
import { SignUpContainer_auth as AuthData } from "talk-auth/__generated__/SignUpContainer_auth.graphql";
import { SetViewMutation, withSetViewMutation } from "talk-auth/mutations";
import { graphql, withFragmentContainer } from "talk-framework/lib/relay";
import { SetViewMutation } from "talk-auth/mutations";
import {
graphql,
MutationProp,
withFragmentContainer,
withMutation,
} from "talk-framework/lib/relay";
import { getViewURL } from "talk-auth/helpers";
import SignUp from "../components/SignUp";
interface Props {
auth: AuthData;
setView: SetViewMutation;
setView: MutationProp<typeof SetViewMutation>;
}
class SignUpContainer extends Component<Props> {
private goToSignIn = () => this.props.setView({ view: "SIGN_IN" });
private goToSignIn = (e: React.MouseEvent) => {
this.props.setView({ view: "SIGN_IN", history: "push" });
if (e.preventDefault) {
e.preventDefault();
}
};
public render() {
const integrations = this.props.auth.integrations;
return (
<SignUp
signInHref={getViewURL("SIGN_IN")}
auth={this.props.auth}
onGotoSignIn={this.goToSignIn}
emailEnabled={
@@ -44,7 +56,7 @@ class SignUpContainer extends Component<Props> {
}
}
const enhanced = withSetViewMutation(
const enhanced = withMutation(SetViewMutation)(
withFragmentContainer<Props>({
auth: graphql`
fragment SignUpContainer_auth on Auth {