Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion frontend/mocking/faked_schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -2518,6 +2518,9 @@ export const getTypeNames = () => gql`
# This mutation allows users to leave a given organization.
leaveOrganization(input: LeaveOrganizationInput!): LeaveOrganizationPayload

# This mutation allows users to close their account.
closeAccount(input: CloseAccountInput!): CloseAccountPayload

# This mutation allows admins or higher to remove users from any organizations they belong to.
removeUserFromOrg(input: RemoveUserFromOrgInput!): RemoveUserFromOrgPayload

Expand Down Expand Up @@ -2655,7 +2658,7 @@ export const getTypeNames = () => gql`
# This union is used with the \`leaveOrganization\` mutation, allowing for users to leave a given organization, and support any errors that may occur.
union LeaveOrganizationUnion = AffiliationError | LeaveOrganizationResult

# This object is used to inform the user that they successful left a given organization.
# This object is used to inform the user that they successfully left a given organization.
type LeaveOrganizationResult {
# Status message confirming the user left the org.
status: String
Expand All @@ -2667,6 +2670,35 @@ export const getTypeNames = () => gql`
clientMutationId: String
}

type CloseAccountPayload {
# \`CloseAccountUnion\` resolving to either a \`CloseAccountResult\` or \`CloseAccountError\`.
result: CloseAccountUnion
clientMutationId: String
}

# This union is used with the \`CloseAccount\` mutation, allowing for users to close their account, and support any errors that may occur.
union CloseAccountUnion = CloseAccountError | CloseAccountResult

# This object is used to inform the user if any errors occurred during closure of an account.
type CloseAccountError {
# Error code to inform user what the issue is related to.
code: Int

# Description of the issue that was encountered.
description: String
}

# This object is used to inform the user that they successfully closed their account.
type CloseAccountResult {
# Status message confirming the user has closed their account.
status: String
}

input CloseAccountInput {
# Id of the user who is closing their account.
userId: ID!
}

type RemoveUserFromOrgPayload {
# \`RemoveUserFromOrgUnion\` returning either a \`RemoveUserFromOrgResult\`, or \`RemoveUserFromOrgError\` object.
result: RemoveUserFromOrgUnion
Expand Down
177 changes: 174 additions & 3 deletions frontend/src/UserPage.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import React, { useState } from 'react'
import { string } from 'prop-types'
import { Button, Divider, SimpleGrid, Stack, useToast } from '@chakra-ui/react'
import {
Button,
Divider,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
SimpleGrid,
Stack,
Text,
useDisclosure,
useToast,
} from '@chakra-ui/react'
import { EmailIcon } from '@chakra-ui/icons'
import { useMutation, useQuery } from '@apollo/client'
import { QUERY_CURRENT_USER } from './graphql/queries'
import { useHistory } from 'react-router-dom'
import { t, Trans } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import EditableUserLanguage from './EditableUserLanguage'
import EditableUserDisplayName from './EditableUserDisplayName'
import EditableUserEmail from './EditableUserEmail'
Expand All @@ -13,10 +30,16 @@ import { LoadingMessage } from './LoadingMessage'
import { ErrorFallbackMessage } from './ErrorFallbackMessage'
import EditableUserTFAMethod from './EditableUserTFAMethod'
import EditableUserPhoneNumber from './EditableUserPhoneNumber'
import { SEND_EMAIL_VERIFICATION } from './graphql/mutations'
import { SEND_EMAIL_VERIFICATION, CLOSE_ACCOUNT } from './graphql/mutations'
import { Formik } from 'formik'
import FormField from './FormField'
import { object, string as yupString } from 'yup'
import { fieldRequirements } from './fieldRequirements'

export default function UserPage() {
const toast = useToast()
const history = useHistory()
const { i18n } = useLingui()
const [emailSent, setEmailSent] = useState(false)
const [sendEmailVerification, { error }] = useMutation(
SEND_EMAIL_VERIFICATION,
Expand Down Expand Up @@ -45,6 +68,63 @@ export default function UserPage() {
},
)

const [closeAccount, { loading: loadingCloseAccount }] = useMutation(
CLOSE_ACCOUNT,
{
onError(error) {
toast({
title: i18n._(t`An error occurred.`),
description: error.message,
status: 'error',
duration: 9000,
isClosable: true,
position: 'top-left',
})
},
onCompleted({ closeAccount }) {
if (closeAccount.result.__typename === 'CloseAccountResult') {
toast({
title: i18n._(t`Account Closed Successfully`),
description: i18n._(
t`Tracker account has been successfully closed.`,
),
status: 'success',
duration: 9000,
isClosable: true,
position: 'top-left',
})
closeAccountOnClose()
history.push('/')
} else if (closeAccount.result.__typename === 'CloseAccountError') {
toast({
title: i18n._(t`Unable to close the account.`),
description: closeAccount.result.description,
status: 'error',
duration: 9000,
isClosable: true,
position: 'top-left',
})
} else {
toast({
title: i18n._(t`Incorrect send method received.`),
description: i18n._(t`Incorrect closeAccount.result typename.`),
status: 'error',
duration: 9000,
isClosable: true,
position: 'top-left',
})
console.log('Incorrect closeAccount.result typename.')
}
},
},
)

const {
isOpen: closeAccountIsOpen,
onOpen: closeAccountOnOpen,
onClose: closeAccountOnClose,
} = useDisclosure()

const {
loading: queryUserLoading,
error: queryUserError,
Expand Down Expand Up @@ -73,8 +153,14 @@ export default function UserPage() {
phoneValidated,
} = queryUserData?.userPage

const closeAccountValidationSchema = object().shape({
userNameConfirm: yupString()
.required(i18n._(fieldRequirements.field.required.message))
.matches(userName, t`User email does not match.`),
})

return (
<SimpleGrid columns={{ md: 1, lg: 2 }} width="100%">
<SimpleGrid columns={{ base: 1, md: 2 }} width="100%">
<Stack py={25} px="4">
<EditableUserDisplayName detailValue={displayName} />

Expand Down Expand Up @@ -114,7 +200,92 @@ export default function UserPage() {
<Trans>Verify Email</Trans>
</Button>
)}

<Divider />

<Button
variant="danger"
onClick={() => {
closeAccountOnOpen()
}}
ml="auto"
w={{ base: '100%', md: 'auto' }}
mb={2}
alignSelf="flex-end"
>
<Trans> Close Account </Trans>
</Button>
</Stack>

<Modal
isOpen={closeAccountIsOpen}
onClose={closeAccountOnClose}
motionPreset="slideInBottom"
>
<Formik
validateOnBlur={false}
initialValues={{
userNameConfirm: '',
}}
initialTouched={{
userNameConfirm: true,
}}
validationSchema={closeAccountValidationSchema}
onSubmit={async () => {
await closeAccount({})
}}
>
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<ModalOverlay />
<ModalContent pb={4}>
<ModalHeader>
<Trans>Close Account</Trans>
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Trans>
This action CANNOT be reversed, are you sure you wish to to
close the account {displayName}?
</Trans>

<Text mb="1rem">
<Trans>
Enter "{userName}" below to confirm removal. This field is
case-sensitive.
</Trans>
</Text>

<FormField
name="userNameConfirm"
label={t`User Email`}
placeholder={userName}
/>
</ModalBody>

<ModalFooter>
<Button
variant="primaryOutline"
mr="4"
onClick={closeAccountOnClose}
>
<Trans>Cancel</Trans>
</Button>

<Button
variant="primary"
mr="4"
type="submit"
isLoading={loadingCloseAccount}
>
<Trans>Confirm</Trans>
</Button>
</ModalFooter>
</ModalContent>
</form>
)}
</Formik>
</Modal>
</SimpleGrid>
)
}
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/graphql/mutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,22 @@ export const VERIFY_ACCOUNT = gql`
}
`

export const CLOSE_ACCOUNT = gql`
mutation CloseAccount($userId: ID) {
closeAccount(input: { userId: $userId }) {
result {
... on CloseAccountError {
code
description
}
... on CloseAccountResult {
status
}
}
}
}
`

export const CREATE_ORGANIZATION = gql`
mutation CreateOrganization(
$acronymEN: Acronym!
Expand Down