mirror of
https://github.com/wassname/talk.git
synced 2026-09-12 13:01:11 +08:00
[CORL-220] Change email address (#2461)
* change email form * add update email mutation * validate new email addresses * add email verification callout * add translation strings * fix submit button logic * rename model methods * style email verifcation box * resend email verificatoin button * show success message on email resend * update user profile email * fix duplicate import * add change email spec * fix profile spacing * update snapshots * update snaps * add preventSubmit function to email change component * update business logic for profile updating * update logic for when users can update email or username * check for less specific duplicate email error * prevent sso-only users from editing email * only allow email and username edit if enabeld in local profile * use generic server error for cannot update profile * remove merge conflict * fix tests * extract logic to get auth integrations * fix merge error
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
.callOut {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.errorIcon {
|
||||
padding-right: var(--spacing-2);
|
||||
padding-top: var(--spacing-1);
|
||||
}
|
||||
|
||||
.successMessage {
|
||||
background-color: var(--palette-primary-lightest);
|
||||
border-color: var(--palette-primary-light);
|
||||
color: var(--palette-text-primary);
|
||||
padding: var(--spacing-2);
|
||||
box-sizing: border-box;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
float: none;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.currentEmail {
|
||||
color: var(--palette-grey-dark);
|
||||
}
|
||||
|
||||
.resendButton {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
import { FORM_ERROR, FormApi, FormState } from "final-form";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { PasswordField } from "coral-framework/components";
|
||||
import getAuthenticationIntegrations from "coral-framework/helpers/getAuthenticationIntegrations";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { colorFromMeta, ValidationMessage } from "coral-framework/lib/form";
|
||||
import { createFetch, useFetch } from "coral-framework/lib/relay";
|
||||
import {
|
||||
graphql,
|
||||
useMutation,
|
||||
withFragmentContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import {
|
||||
composeValidators,
|
||||
required,
|
||||
validateEmail,
|
||||
} from "coral-framework/lib/validation";
|
||||
import {
|
||||
Button,
|
||||
ButtonIcon,
|
||||
CallOut,
|
||||
Flex,
|
||||
FormField,
|
||||
HorizontalGutter,
|
||||
Icon,
|
||||
InputLabel,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "coral-ui/components";
|
||||
|
||||
import { ChangeEmailContainer_settings as SettingsData } from "coral-stream/__generated__/ChangeEmailContainer_settings.graphql";
|
||||
import { ChangeEmailContainer_viewer as ViewerData } from "coral-stream/__generated__/ChangeEmailContainer_viewer.graphql";
|
||||
|
||||
import UpdateEmailMutation from "./UpdateEmailMutation";
|
||||
|
||||
import styles from "./ChangeEmailContainer.css";
|
||||
|
||||
const fetcher = createFetch(
|
||||
"resendConfirmation",
|
||||
(environment: Environment, variables, context) => {
|
||||
return context.rest.fetch<void>("/account/confirm", {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
interface Props {
|
||||
viewer: ViewerData;
|
||||
settings: SettingsData;
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const changeEmailContainer: FunctionComponent<Props> = ({
|
||||
viewer,
|
||||
settings,
|
||||
}) => {
|
||||
const updateEmail = useMutation(UpdateEmailMutation);
|
||||
|
||||
const [showEditForm, setShowEditForm] = useState(false);
|
||||
const [emailUpdated, setEmailUpdated] = useState(false);
|
||||
const [confirmationResent, setConfirmationResent] = useState(false);
|
||||
const makeFetchCall = useFetch(fetcher);
|
||||
const resend = useCallback(async () => {
|
||||
await makeFetchCall();
|
||||
setConfirmationResent(true);
|
||||
}, [fetcher]);
|
||||
|
||||
const toggleEditForm = useCallback(() => {
|
||||
setShowEditForm(!showEditForm);
|
||||
}, [setShowEditForm, showEditForm]);
|
||||
const onSubmit = useCallback(
|
||||
async (input: FormProps, form: FormApi) => {
|
||||
try {
|
||||
await updateEmail({
|
||||
email: input.email,
|
||||
password: input.password,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidRequestError) {
|
||||
return err.invalidArgs;
|
||||
}
|
||||
|
||||
return {
|
||||
[FORM_ERROR]: err.message,
|
||||
};
|
||||
}
|
||||
|
||||
form.reset();
|
||||
setShowEditForm(false);
|
||||
setEmailUpdated(true);
|
||||
|
||||
return;
|
||||
},
|
||||
[updateEmail]
|
||||
);
|
||||
|
||||
const canChangeEmail = useMemo(() => {
|
||||
if (
|
||||
!viewer.profiles.find(profile => profile.__typename === "LocalProfile")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const enabled = getAuthenticationIntegrations(settings.auth, "stream");
|
||||
|
||||
return (
|
||||
enabled.includes("local") ||
|
||||
!(enabled.length === 1 && enabled[0] === "sso")
|
||||
);
|
||||
}, [viewer, settings]);
|
||||
|
||||
const preventSubmit = (
|
||||
formState: Pick<
|
||||
FormState,
|
||||
| "pristine"
|
||||
| "hasSubmitErrors"
|
||||
| "hasValidationErrors"
|
||||
| "dirtySinceLastSubmit"
|
||||
>
|
||||
) => {
|
||||
const {
|
||||
pristine,
|
||||
hasValidationErrors,
|
||||
hasSubmitErrors,
|
||||
dirtySinceLastSubmit,
|
||||
} = formState;
|
||||
return (
|
||||
pristine ||
|
||||
hasValidationErrors ||
|
||||
(hasSubmitErrors && !dirtySinceLastSubmit)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<HorizontalGutter spacing={5} data-testid="profile-changeEmail">
|
||||
{!showEditForm && (
|
||||
<Flex alignItems="center">
|
||||
<Typography>{viewer.email}</Typography>{" "}
|
||||
{!viewer.emailVerified && (
|
||||
<Localized id="profile-changeEmail-unverified">
|
||||
<Typography color="textSecondary">(Unverified)</Typography>
|
||||
</Localized>
|
||||
)}
|
||||
{canChangeEmail && (
|
||||
<Localized id="profile-changeEmail-edit">
|
||||
<Button size="small" color="primary" onClick={toggleEditForm}>
|
||||
Edit
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
{!viewer.emailVerified && emailUpdated && !showEditForm && (
|
||||
<CallOut>
|
||||
<Flex itemGutter>
|
||||
<div>
|
||||
<Icon size="lg">email</Icon>
|
||||
</div>
|
||||
<div>
|
||||
<Localized id="profile-changeEmail-please-verify">
|
||||
<Typography variant="heading3" gutterBottom>
|
||||
Verify your email address
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="profile-changeEmail-please-verify-details"
|
||||
$email={viewer.email}
|
||||
>
|
||||
<Typography>
|
||||
An email has been sent to {viewer.email} to verify your
|
||||
account. You must verify your new email address before it can
|
||||
be used for signing into your account or for email
|
||||
notifications.
|
||||
</Typography>
|
||||
</Localized>
|
||||
|
||||
<Localized id="profile-changeEmail-resend">
|
||||
<Button
|
||||
onClick={resend}
|
||||
className={styles.resendButton}
|
||||
color="primary"
|
||||
>
|
||||
Resend verification
|
||||
</Button>
|
||||
</Localized>
|
||||
</div>
|
||||
</Flex>
|
||||
</CallOut>
|
||||
)}
|
||||
{confirmationResent && (
|
||||
<CallOut fullWidth color="primary">
|
||||
<Localized id="profile-changeEmail-resent">
|
||||
<Typography>Your confirmation email has been re-sent.</Typography>
|
||||
</Localized>
|
||||
</CallOut>
|
||||
)}
|
||||
{showEditForm && (
|
||||
<CallOut className={styles.callOut} color="primary">
|
||||
<HorizontalGutter spacing={4}>
|
||||
<div>
|
||||
<Localized id="profile-changeEmail-heading">
|
||||
<Typography variant="heading2" gutterBottom>
|
||||
Edit your email address
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Localized id="profile-changeEmail-desc">
|
||||
<Typography>
|
||||
Change the email address used for signing in and for receiving
|
||||
communication about your account.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
<div>
|
||||
<Localized id="profile-changeEmail-current">
|
||||
<Typography
|
||||
className={styles.currentEmail}
|
||||
variant="bodyCopyBold"
|
||||
>
|
||||
Current email
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Typography variant="heading2">{viewer.email}</Typography>
|
||||
</div>
|
||||
<Form onSubmit={onSubmit}>
|
||||
{({
|
||||
handleSubmit,
|
||||
submitError,
|
||||
invalid,
|
||||
submitting,
|
||||
...formProps
|
||||
}) => (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
data-testid="profile-changeEmail-form"
|
||||
>
|
||||
<HorizontalGutter spacing={4}>
|
||||
<FormField>
|
||||
<HorizontalGutter>
|
||||
<Localized id="profile-changeEmail-newEmail-label">
|
||||
<InputLabel htmlFor="profile-changeEmail-Email">
|
||||
New email address
|
||||
</InputLabel>
|
||||
</Localized>
|
||||
<Field
|
||||
name="email"
|
||||
validate={composeValidators(required, validateEmail)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<>
|
||||
<TextField
|
||||
{...input}
|
||||
fullWidth
|
||||
id="profile-changeEmail-Email"
|
||||
/>
|
||||
<ValidationMessage meta={meta} />
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</HorizontalGutter>
|
||||
</FormField>
|
||||
<FormField>
|
||||
<HorizontalGutter>
|
||||
<Field
|
||||
name="password"
|
||||
validate={composeValidators(required)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="profile-changeEmail-password">
|
||||
<InputLabel htmlFor={input.name}>
|
||||
Password
|
||||
</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="profile-changeEmail-password-input"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<PasswordField
|
||||
id={input.name}
|
||||
placeholder="Password"
|
||||
color={colorFromMeta(meta)}
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
{...input}
|
||||
/>
|
||||
</Localized>
|
||||
<ValidationMessage meta={meta} fullWidth />
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
</HorizontalGutter>
|
||||
</FormField>
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
<Flex justifyContent="flex-end" className={styles.footer}>
|
||||
<Localized id="profile-changeEmail-cancel">
|
||||
<Button type="button" onClick={toggleEditForm}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
<Localized id="profile-changeEmail-submit">
|
||||
<Button
|
||||
variant={
|
||||
preventSubmit(formProps) ? "outlined" : "filled"
|
||||
}
|
||||
type="submit"
|
||||
color={preventSubmit(formProps) ? "regular" : "primary"}
|
||||
disabled={preventSubmit(formProps)}
|
||||
>
|
||||
<ButtonIcon>save</ButtonIcon>
|
||||
<span>Save</span>
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</HorizontalGutter>
|
||||
</CallOut>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
viewer: graphql`
|
||||
fragment ChangeEmailContainer_viewer on User {
|
||||
email
|
||||
emailVerified
|
||||
profiles {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ChangeEmailContainer_settings on Settings {
|
||||
auth {
|
||||
integrations {
|
||||
local {
|
||||
enabled
|
||||
targetFilter {
|
||||
stream
|
||||
}
|
||||
}
|
||||
google {
|
||||
enabled
|
||||
targetFilter {
|
||||
stream
|
||||
}
|
||||
}
|
||||
oidc {
|
||||
enabled
|
||||
targetFilter {
|
||||
stream
|
||||
}
|
||||
}
|
||||
sso {
|
||||
enabled
|
||||
targetFilter {
|
||||
stream
|
||||
}
|
||||
}
|
||||
facebook {
|
||||
enabled
|
||||
targetFilter {
|
||||
stream
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(changeEmailContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { UpdateEmailMutation as MutationTypes } from "coral-stream/__generated__/UpdateEmailMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const UpdateEmailMutation = createMutation(
|
||||
"updateEmail",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation UpdateEmailMutation($input: UpdateEmailInput!) {
|
||||
updateEmail(input: $input) {
|
||||
clientMutationId
|
||||
user {
|
||||
email
|
||||
emailVerified
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
updateEmail: {
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
user: {
|
||||
email: input.email,
|
||||
emailVerified: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default UpdateEmailMutation;
|
||||
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
default,
|
||||
default as ChangeEmailContainer,
|
||||
} from "./ChangeEmailContainer";
|
||||
@@ -11,6 +11,7 @@ import { Field, Form } from "react-final-form";
|
||||
|
||||
import { ALLOWED_USERNAME_CHANGE_FREQUENCY } from "coral-common/constants";
|
||||
import { reduceSeconds, UNIT } from "coral-common/helpers/i18n";
|
||||
import getAuthenticationIntegrations from "coral-framework/helpers/getAuthenticationIntegrations";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { ValidationMessage } from "coral-framework/lib/form";
|
||||
import {
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
validateUsername,
|
||||
validateUsernameEquals,
|
||||
} from "coral-framework/lib/validation";
|
||||
import { ChangeUsernameContainer_settings as SettingsData } from "coral-stream/__generated__/ChangeUsernameContainer_settings.graphql";
|
||||
import { ChangeUsernameContainer_viewer as ViewerData } from "coral-stream/__generated__/ChangeUsernameContainer_viewer.graphql";
|
||||
import {
|
||||
Box,
|
||||
@@ -50,6 +52,7 @@ const FREQUENCYSCALED = reduceSeconds(ALLOWED_USERNAME_CHANGE_FREQUENCY, [
|
||||
|
||||
interface Props {
|
||||
viewer: ViewerData;
|
||||
settings: SettingsData;
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
@@ -57,7 +60,10 @@ interface FormProps {
|
||||
usernameConfirm: string;
|
||||
}
|
||||
|
||||
const ChangeUsernameContainer: FunctionComponent<Props> = ({ viewer }) => {
|
||||
const ChangeUsernameContainer: FunctionComponent<Props> = ({
|
||||
viewer,
|
||||
settings,
|
||||
}) => {
|
||||
const [showEditForm, setShowEditForm] = useState(false);
|
||||
const [showSuccessMessage, setShowSuccessMessage] = useState(false);
|
||||
const toggleEditForm = useCallback(() => {
|
||||
@@ -69,6 +75,20 @@ const ChangeUsernameContainer: FunctionComponent<Props> = ({ viewer }) => {
|
||||
setShowEditForm,
|
||||
]);
|
||||
|
||||
const canChangeLocalAuth = useMemo(() => {
|
||||
if (
|
||||
!viewer.profiles.find(profile => profile.__typename === "LocalProfile")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const enabled = getAuthenticationIntegrations(settings.auth, "stream");
|
||||
|
||||
return (
|
||||
enabled.includes("local") ||
|
||||
!(enabled.length === 1 && enabled[0] === "sso")
|
||||
);
|
||||
}, [viewer, settings]);
|
||||
|
||||
const canChangeUsername = useMemo(() => {
|
||||
const { username } = viewer.status;
|
||||
if (username && username.history.length > 1) {
|
||||
@@ -148,11 +168,13 @@ const ChangeUsernameContainer: FunctionComponent<Props> = ({ viewer }) => {
|
||||
{!showEditForm && (
|
||||
<Flex alignItems="baseline">
|
||||
<Typography variant="header2">{viewer.username}</Typography>
|
||||
<Localized id="profile-changeUsername-edit">
|
||||
<Button size="small" color="primary" onClick={toggleEditForm}>
|
||||
edit
|
||||
</Button>
|
||||
</Localized>
|
||||
{canChangeLocalAuth && (
|
||||
<Localized id="profile-changeUsername-edit">
|
||||
<Button size="small" color="primary" onClick={toggleEditForm}>
|
||||
edit
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
{showEditForm && (
|
||||
@@ -337,6 +359,9 @@ const enhanced = withFragmentContainer<Props>({
|
||||
viewer: graphql`
|
||||
fragment ChangeUsernameContainer_viewer on User {
|
||||
username
|
||||
profiles {
|
||||
__typename
|
||||
}
|
||||
status {
|
||||
username {
|
||||
history {
|
||||
@@ -347,6 +372,44 @@ const enhanced = withFragmentContainer<Props>({
|
||||
}
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ChangeUsernameContainer_settings on Settings {
|
||||
auth {
|
||||
integrations {
|
||||
local {
|
||||
enabled
|
||||
targetFilter {
|
||||
stream
|
||||
}
|
||||
}
|
||||
google {
|
||||
enabled
|
||||
targetFilter {
|
||||
stream
|
||||
}
|
||||
}
|
||||
oidc {
|
||||
enabled
|
||||
targetFilter {
|
||||
stream
|
||||
}
|
||||
}
|
||||
sso {
|
||||
enabled
|
||||
targetFilter {
|
||||
stream
|
||||
}
|
||||
}
|
||||
facebook {
|
||||
enabled
|
||||
targetFilter {
|
||||
stream
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(ChangeUsernameContainer);
|
||||
|
||||
export default enhanced;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
|
||||
import { graphql, useLocal } from "coral-framework/lib/relay";
|
||||
@@ -11,8 +12,8 @@ import {
|
||||
TabContent,
|
||||
TabPane,
|
||||
} from "coral-ui/components";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
|
||||
import ChangeEmailContainer from "./ChangeEmail";
|
||||
import ChangeUsernameContainer from "./ChangeUsername";
|
||||
import CommentHistoryContainer from "./CommentHistory";
|
||||
import SettingsContainer from "./Settings";
|
||||
@@ -22,9 +23,13 @@ export interface ProfileProps {
|
||||
viewer: PropTypesOf<typeof UserBoxContainer>["viewer"] &
|
||||
PropTypesOf<typeof CommentHistoryContainer>["viewer"] &
|
||||
PropTypesOf<typeof SettingsContainer>["viewer"] &
|
||||
PropTypesOf<typeof ChangeUsernameContainer>["viewer"];
|
||||
PropTypesOf<typeof ChangeUsernameContainer>["viewer"] &
|
||||
PropTypesOf<typeof ChangeEmailContainer>["viewer"] &
|
||||
PropTypesOf<typeof SettingsContainer>["viewer"];
|
||||
settings: PropTypesOf<typeof UserBoxContainer>["settings"] &
|
||||
PropTypesOf<typeof SettingsContainer>["settings"];
|
||||
PropTypesOf<typeof ChangeEmailContainer>["settings"] &
|
||||
PropTypesOf<typeof SettingsContainer>["settings"] &
|
||||
PropTypesOf<typeof ChangeUsernameContainer>["settings"];
|
||||
}
|
||||
|
||||
const Profile: FunctionComponent<ProfileProps> = props => {
|
||||
@@ -39,7 +44,13 @@ const Profile: FunctionComponent<ProfileProps> = props => {
|
||||
);
|
||||
return (
|
||||
<HorizontalGutter spacing={5}>
|
||||
<ChangeUsernameContainer viewer={props.viewer} />
|
||||
<HorizontalGutter spacing={2}>
|
||||
<ChangeUsernameContainer
|
||||
settings={props.settings}
|
||||
viewer={props.viewer}
|
||||
/>
|
||||
<ChangeEmailContainer settings={props.settings} viewer={props.viewer} />
|
||||
</HorizontalGutter>
|
||||
<TabBar
|
||||
variant="secondary"
|
||||
activeTab={local.profileTab}
|
||||
|
||||
@@ -37,11 +37,14 @@ const enhanced = withFragmentContainer<ProfileContainerProps>({
|
||||
...CommentHistoryContainer_viewer
|
||||
...SettingsContainer_viewer
|
||||
...ChangeUsernameContainer_viewer
|
||||
...ChangeEmailContainer_viewer
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ProfileContainer_settings on Settings {
|
||||
...UserBoxContainer_settings
|
||||
...ChangeEmailContainer_settings
|
||||
...ChangeUsernameContainer_settings
|
||||
...SettingsContainer_settings
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -113,6 +113,8 @@ export const settingsWithoutLocalAuth = createFixture<GQLSettings>(
|
||||
|
||||
export const baseUser = createFixture<GQLUser>({
|
||||
createdAt: "2018-02-06T18:24:00.000Z",
|
||||
id: "base-user",
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
status: {
|
||||
current: [GQLUSER_STATUS.ACTIVE],
|
||||
username: {
|
||||
@@ -130,6 +132,11 @@ export const baseUser = createFixture<GQLUser>({
|
||||
},
|
||||
},
|
||||
ignoreable: true,
|
||||
profiles: [
|
||||
{
|
||||
__typename: "LocalProfile",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const yesterday = new Date();
|
||||
@@ -184,6 +191,14 @@ export const userWithChangedUsername = createFixture<GQLUser>(
|
||||
baseUser
|
||||
);
|
||||
|
||||
export const userWithEmail = createFixture<GQLUser>(
|
||||
{
|
||||
id: "email-user",
|
||||
email: "used-email@email.com",
|
||||
},
|
||||
baseUser
|
||||
);
|
||||
|
||||
export const commenters = createFixtures<GQLUser>(
|
||||
[
|
||||
{
|
||||
|
||||
@@ -66,29 +66,65 @@ exports[`renders the empty settings pane 1`] = `
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-5"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-5"
|
||||
data-testid="profile-changeUsername"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-2"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignBaseline"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-5"
|
||||
data-testid="profile-changeUsername"
|
||||
>
|
||||
<h1
|
||||
className="Box-root Typography-root Typography-header2 Typography-colorTextPrimary"
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignBaseline"
|
||||
>
|
||||
Passivo
|
||||
</h1>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantRegular"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
<h1
|
||||
className="Box-root Typography-root Typography-header2 Typography-colorTextPrimary"
|
||||
>
|
||||
Passivo
|
||||
</h1>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantRegular"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-5"
|
||||
data-testid="profile-changeEmail"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
edit
|
||||
</button>
|
||||
<p
|
||||
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
/>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<p
|
||||
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextSecondary"
|
||||
>
|
||||
(Unverified)
|
||||
</p>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantRegular"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { ReactTestRenderer } from "react-test-renderer";
|
||||
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
act,
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import { baseUser, settings, stories } from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
const story = stories[0];
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context } = create({
|
||||
...params,
|
||||
resolvers: pureMerge(
|
||||
createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () => settings,
|
||||
viewer: () => baseUser,
|
||||
story: () => story,
|
||||
},
|
||||
}),
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue("SETTINGS", "profileTab");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
testRenderer,
|
||||
context,
|
||||
};
|
||||
}
|
||||
|
||||
describe("change email form", () => {
|
||||
let testRenderer: ReactTestRenderer;
|
||||
beforeEach(async () => {
|
||||
const setup = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
viewer: () => baseUser,
|
||||
},
|
||||
Mutation: {
|
||||
updateEmail: ({ variables }) => {
|
||||
expectAndFail(variables).toMatchObject({
|
||||
email: "updated_email@test.com",
|
||||
});
|
||||
return {
|
||||
user: {
|
||||
...baseUser,
|
||||
email: "updated_email@test.com",
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
testRenderer = setup.testRenderer;
|
||||
});
|
||||
|
||||
it("ensures email field is required", async () => {
|
||||
const changeEmail = within(testRenderer.root).getByTestID(
|
||||
"profile-changeEmail"
|
||||
);
|
||||
const editButton = within(changeEmail).getByText("Edit");
|
||||
act(() => {
|
||||
editButton.props.onClick();
|
||||
});
|
||||
const form = within(changeEmail).getByType("form");
|
||||
act(() => {
|
||||
form.props.onSubmit();
|
||||
});
|
||||
within(changeEmail).getAllByText("This field is required", {
|
||||
exact: false,
|
||||
});
|
||||
const button = within(changeEmail).getByText("Save");
|
||||
expect(button.props.disabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it("ensures password field is required", async () => {
|
||||
const changeEmail = within(testRenderer.root).getByTestID(
|
||||
"profile-changeEmail"
|
||||
);
|
||||
const editButton = within(changeEmail).getByText("Edit");
|
||||
act(() => {
|
||||
editButton.props.onClick();
|
||||
});
|
||||
const form = within(changeEmail).getByType("form");
|
||||
const emailInput = within(changeEmail).getByLabelText("New email address", {
|
||||
exact: false,
|
||||
});
|
||||
act(() => {
|
||||
emailInput.props.onChange("test@test.com");
|
||||
form.props.onSubmit();
|
||||
});
|
||||
within(changeEmail).getByText("This field is required", {
|
||||
exact: false,
|
||||
});
|
||||
const button = within(changeEmail).getByText("Save");
|
||||
expect(button.props.disabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it("updates email if fields are valid", async () => {
|
||||
const changeEmail = within(testRenderer.root).getByTestID(
|
||||
"profile-changeEmail"
|
||||
);
|
||||
const editButton = within(changeEmail).getByText("Edit");
|
||||
act(() => {
|
||||
editButton.props.onClick();
|
||||
});
|
||||
const form = within(changeEmail).getByType("form");
|
||||
const emailInput = within(changeEmail).getByLabelText("New email address", {
|
||||
exact: false,
|
||||
});
|
||||
const password = within(changeEmail).getByLabelText("Password");
|
||||
await act(async () => {
|
||||
emailInput.props.onChange("updated_email@test.com");
|
||||
password.props.onChange("test");
|
||||
await form.props.onSubmit();
|
||||
});
|
||||
|
||||
within(changeEmail).getAllByText("updated_email@test.com", {
|
||||
exact: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user