diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index c533150d5..a311b6b6d 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -252,28 +252,6 @@ export enum ContentReader { Web = 'WEB' } -export type ConvertToEmailError = { - __typename?: 'ConvertToEmailError'; - errorCodes: Array; -}; - -export enum ConvertToEmailErrorCode { - BadRequest = 'BAD_REQUEST', - EmailAlreadyExists = 'EMAIL_ALREADY_EXISTS', - Unauthorized = 'UNAUTHORIZED' -} - -export type ConvertToEmailInput = { - email: Scalars['String']; -}; - -export type ConvertToEmailResult = ConvertToEmailError | ConvertToEmailSuccess; - -export type ConvertToEmailSuccess = { - __typename?: 'ConvertToEmailSuccess'; - success: Scalars['Boolean']; -}; - export type CreateArticleError = { __typename?: 'CreateArticleError'; errorCodes: Array; @@ -1257,7 +1235,6 @@ export type Mutation = { __typename?: 'Mutation'; addPopularRead: AddPopularReadResult; bulkAction: BulkActionResult; - convertToEmail: ConvertToEmailResult; createArticle: CreateArticleResult; createArticleSavingRequest: CreateArticleSavingRequestResult; createGroup: CreateGroupResult; @@ -1314,6 +1291,7 @@ export type Mutation = { setWebhook: SetWebhookResult; subscribe: SubscribeResult; unsubscribe: UnsubscribeResult; + updateEmail: UpdateEmailResult; updateFilter: UpdateFilterResult; updateHighlight: UpdateHighlightResult; updateHighlightReply: UpdateHighlightReplyResult; @@ -1344,11 +1322,6 @@ export type MutationBulkActionArgs = { }; -export type MutationConvertToEmailArgs = { - input: ConvertToEmailInput; -}; - - export type MutationCreateArticleArgs = { input: CreateArticleInput; }; @@ -1620,6 +1593,11 @@ export type MutationUnsubscribeArgs = { }; +export type MutationUpdateEmailArgs = { + input: UpdateEmailInput; +}; + + export type MutationUpdateFilterArgs = { input: UpdateFilterInput; }; @@ -2873,6 +2851,29 @@ export type UnsubscribeSuccess = { subscription: Subscription; }; +export type UpdateEmailError = { + __typename?: 'UpdateEmailError'; + errorCodes: Array; +}; + +export enum UpdateEmailErrorCode { + BadRequest = 'BAD_REQUEST', + EmailAlreadyExists = 'EMAIL_ALREADY_EXISTS', + Unauthorized = 'UNAUTHORIZED' +} + +export type UpdateEmailInput = { + email: Scalars['String']; +}; + +export type UpdateEmailResult = UpdateEmailError | UpdateEmailSuccess; + +export type UpdateEmailSuccess = { + __typename?: 'UpdateEmailSuccess'; + email: Scalars['String']; + verificationEmailSent?: Maybe; +}; + export type UpdateFilterError = { __typename?: 'UpdateFilterError'; errorCodes: Array; @@ -3239,6 +3240,7 @@ export enum UploadImportFileType { export type User = { __typename?: 'User'; + email?: Maybe; followersCount?: Maybe; friendsCount?: Maybe; id: Scalars['ID']; @@ -3252,6 +3254,7 @@ export type User = { sharedArticlesCount?: Maybe; sharedHighlightsCount?: Maybe; sharedNotesCount?: Maybe; + source?: Maybe; viewerIsFollowing?: Maybe; }; @@ -3470,11 +3473,6 @@ export type ResolversTypes = { BulkActionSuccess: ResolverTypeWrapper; BulkActionType: BulkActionType; ContentReader: ContentReader; - ConvertToEmailError: ResolverTypeWrapper; - ConvertToEmailErrorCode: ConvertToEmailErrorCode; - ConvertToEmailInput: ConvertToEmailInput; - ConvertToEmailResult: ResolversTypes['ConvertToEmailError'] | ResolversTypes['ConvertToEmailSuccess']; - ConvertToEmailSuccess: ResolverTypeWrapper; CreateArticleError: ResolverTypeWrapper; CreateArticleErrorCode: CreateArticleErrorCode; CreateArticleInput: CreateArticleInput; @@ -3846,6 +3844,11 @@ export type ResolversTypes = { UnsubscribeErrorCode: UnsubscribeErrorCode; UnsubscribeResult: ResolversTypes['UnsubscribeError'] | ResolversTypes['UnsubscribeSuccess']; UnsubscribeSuccess: ResolverTypeWrapper; + UpdateEmailError: ResolverTypeWrapper; + UpdateEmailErrorCode: UpdateEmailErrorCode; + UpdateEmailInput: UpdateEmailInput; + UpdateEmailResult: ResolversTypes['UpdateEmailError'] | ResolversTypes['UpdateEmailSuccess']; + UpdateEmailSuccess: ResolverTypeWrapper; UpdateFilterError: ResolverTypeWrapper; UpdateFilterErrorCode: UpdateFilterErrorCode; UpdateFilterInput: UpdateFilterInput; @@ -3969,10 +3972,6 @@ export type ResolversParentTypes = { BulkActionError: BulkActionError; BulkActionResult: ResolversParentTypes['BulkActionError'] | ResolversParentTypes['BulkActionSuccess']; BulkActionSuccess: BulkActionSuccess; - ConvertToEmailError: ConvertToEmailError; - ConvertToEmailInput: ConvertToEmailInput; - ConvertToEmailResult: ResolversParentTypes['ConvertToEmailError'] | ResolversParentTypes['ConvertToEmailSuccess']; - ConvertToEmailSuccess: ConvertToEmailSuccess; CreateArticleError: CreateArticleError; CreateArticleInput: CreateArticleInput; CreateArticleResult: ResolversParentTypes['CreateArticleError'] | ResolversParentTypes['CreateArticleSuccess']; @@ -4263,6 +4262,10 @@ export type ResolversParentTypes = { UnsubscribeError: UnsubscribeError; UnsubscribeResult: ResolversParentTypes['UnsubscribeError'] | ResolversParentTypes['UnsubscribeSuccess']; UnsubscribeSuccess: UnsubscribeSuccess; + UpdateEmailError: UpdateEmailError; + UpdateEmailInput: UpdateEmailInput; + UpdateEmailResult: ResolversParentTypes['UpdateEmailError'] | ResolversParentTypes['UpdateEmailSuccess']; + UpdateEmailSuccess: UpdateEmailSuccess; UpdateFilterError: UpdateFilterError; UpdateFilterInput: UpdateFilterInput; UpdateFilterResult: ResolversParentTypes['UpdateFilterError'] | ResolversParentTypes['UpdateFilterSuccess']; @@ -4519,20 +4522,6 @@ export type BulkActionSuccessResolvers; }; -export type ConvertToEmailErrorResolvers = { - errorCodes?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}; - -export type ConvertToEmailResultResolvers = { - __resolveType: TypeResolveFn<'ConvertToEmailError' | 'ConvertToEmailSuccess', ParentType, ContextType>; -}; - -export type ConvertToEmailSuccessResolvers = { - success?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}; - export type CreateArticleErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -5242,7 +5231,6 @@ export type MoveLabelSuccessResolvers = { addPopularRead?: Resolver>; bulkAction?: Resolver>; - convertToEmail?: Resolver>; createArticle?: Resolver>; createArticleSavingRequest?: Resolver>; createGroup?: Resolver>; @@ -5299,6 +5287,7 @@ export type MutationResolvers>; subscribe?: Resolver>; unsubscribe?: Resolver>; + updateEmail?: Resolver>; updateFilter?: Resolver>; updateHighlight?: Resolver>; updateHighlightReply?: Resolver>; @@ -6007,6 +5996,21 @@ export type UnsubscribeSuccessResolvers; }; +export type UpdateEmailErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type UpdateEmailResultResolvers = { + __resolveType: TypeResolveFn<'UpdateEmailError' | 'UpdateEmailSuccess', ParentType, ContextType>; +}; + +export type UpdateEmailSuccessResolvers = { + email?: Resolver; + verificationEmailSent?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type UpdateFilterErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -6209,6 +6213,7 @@ export type UploadImportFileSuccessResolvers = { + email?: Resolver, ParentType, ContextType>; followersCount?: Resolver, ParentType, ContextType>; friendsCount?: Resolver, ParentType, ContextType>; id?: Resolver; @@ -6221,6 +6226,7 @@ export type UserResolvers, ParentType, ContextType>; sharedHighlightsCount?: Resolver, ParentType, ContextType>; sharedNotesCount?: Resolver, ParentType, ContextType>; + source?: Resolver, ParentType, ContextType>; viewerIsFollowing?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -6334,9 +6340,6 @@ export type Resolvers = { BulkActionError?: BulkActionErrorResolvers; BulkActionResult?: BulkActionResultResolvers; BulkActionSuccess?: BulkActionSuccessResolvers; - ConvertToEmailError?: ConvertToEmailErrorResolvers; - ConvertToEmailResult?: ConvertToEmailResultResolvers; - ConvertToEmailSuccess?: ConvertToEmailSuccessResolvers; CreateArticleError?: CreateArticleErrorResolvers; CreateArticleResult?: CreateArticleResultResolvers; CreateArticleSavingRequestError?: CreateArticleSavingRequestErrorResolvers; @@ -6583,6 +6586,9 @@ export type Resolvers = { UnsubscribeError?: UnsubscribeErrorResolvers; UnsubscribeResult?: UnsubscribeResultResolvers; UnsubscribeSuccess?: UnsubscribeSuccessResolvers; + UpdateEmailError?: UpdateEmailErrorResolvers; + UpdateEmailResult?: UpdateEmailResultResolvers; + UpdateEmailSuccess?: UpdateEmailSuccessResolvers; UpdateFilterError?: UpdateFilterErrorResolvers; UpdateFilterResult?: UpdateFilterResultResolvers; UpdateFilterSuccess?: UpdateFilterSuccessResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 1cb0af004..1af5e9d49 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -211,26 +211,6 @@ enum ContentReader { WEB } -type ConvertToEmailError { - errorCodes: [ConvertToEmailErrorCode!]! -} - -enum ConvertToEmailErrorCode { - BAD_REQUEST - EMAIL_ALREADY_EXISTS - UNAUTHORIZED -} - -input ConvertToEmailInput { - email: String! -} - -union ConvertToEmailResult = ConvertToEmailError | ConvertToEmailSuccess - -type ConvertToEmailSuccess { - success: Boolean! -} - type CreateArticleError { errorCodes: [CreateArticleErrorCode!]! } @@ -1123,7 +1103,6 @@ type MoveLabelSuccess { type Mutation { addPopularRead(name: String!): AddPopularReadResult! bulkAction(action: BulkActionType!, async: Boolean, expectedCount: Int, labelIds: [ID!], query: String!): BulkActionResult! - convertToEmail(input: ConvertToEmailInput!): ConvertToEmailResult! createArticle(input: CreateArticleInput!): CreateArticleResult! createArticleSavingRequest(input: CreateArticleSavingRequestInput!): CreateArticleSavingRequestResult! createGroup(input: CreateGroupInput!): CreateGroupResult! @@ -1180,6 +1159,7 @@ type Mutation { setWebhook(input: SetWebhookInput!): SetWebhookResult! subscribe(input: SubscribeInput!): SubscribeResult! unsubscribe(name: String!, subscriptionId: ID): UnsubscribeResult! + updateEmail(input: UpdateEmailInput!): UpdateEmailResult! updateFilter(input: UpdateFilterInput!): UpdateFilterResult! updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult! updateHighlightReply(input: UpdateHighlightReplyInput!): UpdateHighlightReplyResult! @@ -2198,6 +2178,27 @@ type UnsubscribeSuccess { subscription: Subscription! } +type UpdateEmailError { + errorCodes: [UpdateEmailErrorCode!]! +} + +enum UpdateEmailErrorCode { + BAD_REQUEST + EMAIL_ALREADY_EXISTS + UNAUTHORIZED +} + +input UpdateEmailInput { + email: String! +} + +union UpdateEmailResult = UpdateEmailError | UpdateEmailSuccess + +type UpdateEmailSuccess { + email: String! + verificationEmailSent: Boolean +} + type UpdateFilterError { errorCodes: [UpdateFilterErrorCode!]! } @@ -2535,6 +2536,7 @@ enum UploadImportFileType { } type User { + email: String followersCount: Int friendsCount: Int id: ID! @@ -2547,6 +2549,7 @@ type User { sharedArticlesCount: Int sharedHighlightsCount: Int sharedNotesCount: Int + source: String viewerIsFollowing: Boolean } diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index a76642524..9c62aeccf 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -124,7 +124,7 @@ import { createReactionResolver, deleteReactionResolver } from './reaction' import { markEmailAsItemResolver, recentEmailsResolver } from './recent_emails' import { recentSearchesResolver } from './recent_searches' import { Claims, WithDataSourcesContext } from './types' -import { convertToEmailResolver } from './user' +import { updateEmailResolver } from './user' /* eslint-disable @typescript-eslint/naming-convention */ type ResultResolveType = { @@ -213,7 +213,7 @@ export const functionResolvers = { setFavoriteArticle: setFavoriteArticleResolver, updateSubscription: updateSubscriptionResolver, updateFilter: updateFilterResolver, - convertToEmail: convertToEmailResolver, + updateEmail: updateEmailResolver, }, Query: { me: getMeUserResolver, @@ -680,5 +680,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('ImportFromIntegration'), ...resultResolveTypeResolver('SetFavoriteArticle'), ...resultResolveTypeResolver('UpdateSubscription'), - ...resultResolveTypeResolver('ConvertToEmail'), + ...resultResolveTypeResolver('UpdateEmail'), } diff --git a/packages/api/src/resolvers/user/index.ts b/packages/api/src/resolvers/user/index.ts index 03336f9b5..62b24a794 100644 --- a/packages/api/src/resolvers/user/index.ts +++ b/packages/api/src/resolvers/user/index.ts @@ -1,13 +1,10 @@ import * as jwt from 'jsonwebtoken' -import { Not } from 'typeorm' +import { RegistrationType } from '../../datalayer/user/model' import { deletePagesByParam } from '../../elastic/pages' import { User as UserEntity } from '../../entity/user' import { getRepository, setClaims } from '../../entity/utils' import { env } from '../../env' import { - ConvertToEmailError, - ConvertToEmailErrorCode, - ConvertToEmailSuccess, DeleteAccountError, DeleteAccountErrorCode, DeleteAccountSuccess, @@ -16,16 +13,19 @@ import { LoginResult, LogOutErrorCode, LogOutResult, - MutationConvertToEmailArgs, MutationDeleteAccountArgs, MutationGoogleLoginArgs, MutationGoogleSignupArgs, + MutationUpdateEmailArgs, MutationUpdateUserArgs, MutationUpdateUserProfileArgs, QueryUserArgs, QueryValidateUsernameArgs, ResolverFn, SignupErrorCode, + UpdateEmailError, + UpdateEmailErrorCode, + UpdateEmailSuccess, UpdateUserError, UpdateUserErrorCode, UpdateUserProfileError, @@ -344,34 +344,49 @@ export const deleteAccountResolver = authorized< return { userID } }) -export const convertToEmailResolver = authorized< - ConvertToEmailSuccess, - ConvertToEmailError, - MutationConvertToEmailArgs +export const updateEmailResolver = authorized< + UpdateEmailSuccess, + UpdateEmailError, + MutationUpdateEmailArgs >(async (_, { input: { email } }, { uid, log }) => { try { const user = await getRepository(UserEntity).findOneBy({ id: uid, - source: Not('EMAIL'), }) if (!user) { return { - errorCodes: [ConvertToEmailErrorCode.Unauthorized], + errorCodes: [UpdateEmailErrorCode.Unauthorized], } } + if (user.source === RegistrationType.Email) { + await AppDataSource.transaction(async (entityManager) => { + await setClaims(entityManager, user.id) + return entityManager.getRepository(UserEntity).update(user.id, { + email, + }) + }) + + return { email } + } + const result = await sendVerificationEmail({ id: user.id, name: user.name, email, }) + if (!result) { + return { + errorCodes: [UpdateEmailErrorCode.BadRequest], + } + } - return { success: result } + return { email, verificationEmailSent: true } } catch (error) { - log.error('Error converting user to email', error) + log.error('Error updating email', error) return { - errorCodes: [ConvertToEmailErrorCode.BadRequest], + errorCodes: [UpdateEmailErrorCode.BadRequest], } } }) diff --git a/packages/api/src/routers/auth/auth_router.ts b/packages/api/src/routers/auth/auth_router.ts index 7b95085d3..b0f6d821e 100644 --- a/packages/api/src/routers/auth/auth_router.ts +++ b/packages/api/src/routers/auth/auth_router.ts @@ -672,7 +672,6 @@ export function authRouter() { const user = await getRepository(User).findOneBy({ id: claims.uid, - source: 'EMAIL', }) if (!user) { return res.redirect( @@ -686,13 +685,17 @@ export function authRouter() { ) } + // check if email needs to be updated + const updateEmail = claims.email && claims.email !== user.email + const hashedPassword = await hashPassword(password) const updated = await AppDataSource.transaction( async (entityManager) => { await setClaims(entityManager, user.id) return entityManager.getRepository(User).update(user.id, { password: hashedPassword, - email: claims.email, + email: updateEmail ? claims.email : undefined, + source: updateEmail ? RegistrationType.Email : undefined, }) } ) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index ba6bc424f..d0b161eb9 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -84,6 +84,8 @@ const schema = gql` sharedNotesCount: Int friendsCount: Int followersCount: Int + email: String + source: String } type Profile { @@ -2567,23 +2569,24 @@ const schema = gql` NOT_FOUND } - union ConvertToEmailResult = ConvertToEmailSuccess | ConvertToEmailError + union UpdateEmailResult = UpdateEmailSuccess | UpdateEmailError - type ConvertToEmailSuccess { - success: Boolean! + type UpdateEmailSuccess { + email: String! + verificationEmailSent: Boolean } - type ConvertToEmailError { - errorCodes: [ConvertToEmailErrorCode!]! + type UpdateEmailError { + errorCodes: [UpdateEmailErrorCode!]! } - enum ConvertToEmailErrorCode { + enum UpdateEmailErrorCode { UNAUTHORIZED BAD_REQUEST EMAIL_ALREADY_EXISTS } - input ConvertToEmailInput { + input UpdateEmailInput { email: String! } @@ -2595,7 +2598,7 @@ const schema = gql` deleteAccount(userID: ID!): DeleteAccountResult! updateUser(input: UpdateUserInput!): UpdateUserResult! updateUserProfile(input: UpdateUserProfileInput!): UpdateUserProfileResult! - convertToEmail(input: ConvertToEmailInput!): ConvertToEmailResult! + updateEmail(input: UpdateEmailInput!): UpdateEmailResult! createArticle(input: CreateArticleInput!): CreateArticleResult! createHighlight(input: CreateHighlightInput!): CreateHighlightResult! mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult! diff --git a/packages/api/src/services/send_emails.ts b/packages/api/src/services/send_emails.ts index 9a81dab55..279b193f9 100644 --- a/packages/api/src/services/send_emails.ts +++ b/packages/api/src/services/send_emails.ts @@ -31,7 +31,7 @@ export const sendVerificationEmail = async (user: { }): Promise => { // generate verification link const token = generateVerificationToken({ id: user.id, email: user.email }) - const link = `${env.client.url}/auth/convert-to-email/${token}` + const link = `${env.client.url}/auth/reset-password/${token}` // send email const dynamicTemplateData = { name: user.name, diff --git a/packages/web/lib/networking/mutations/updateEmailMutation.ts b/packages/web/lib/networking/mutations/updateEmailMutation.ts new file mode 100644 index 000000000..7c6e13bb7 --- /dev/null +++ b/packages/web/lib/networking/mutations/updateEmailMutation.ts @@ -0,0 +1,45 @@ +import { gql } from 'graphql-request' +import { gqlFetcher } from '../networkHelpers' + +export interface UpdateEmailInput { + email: string +} + +export interface UpdateEmailSuccess { + email: string + verificationEmailSent: boolean +} + +interface Response { + updateEmail: UpdateEmailSuccess +} + +export async function updateEmailMutation( + input: UpdateEmailInput +): Promise { + const mutation = gql` + mutation UpdateEmail($input: UpdateEmailInput!) { + updateEmail(input: $input) { + ... on UpdateEmailSuccess { + email + verificationEmailSent + } + ... on UpdateEmailError { + errorCodes + } + } + } + ` + try { + const data = await gqlFetcher(mutation, { + input, + }) + const output = data as Response + return { + email: output.updateEmail.email, + verificationEmailSent: output.updateEmail.verificationEmailSent, + } + } catch (err) { + return undefined + } +} diff --git a/packages/web/lib/networking/queries/useGetViewerQuery.tsx b/packages/web/lib/networking/queries/useGetViewerQuery.tsx index bddd23975..0cf7f9c2d 100644 --- a/packages/web/lib/networking/queries/useGetViewerQuery.tsx +++ b/packages/web/lib/networking/queries/useGetViewerQuery.tsx @@ -17,6 +17,8 @@ export type UserBasicData = { name: string isFullUser?: boolean profile: UserProfile + email: string + source: string } export type UserProfile = { @@ -39,6 +41,8 @@ export function useGetViewerQuery(): ViewerQueryResponse { pictureUrl bio } + email + source } } ` diff --git a/packages/web/pages/settings/account.tsx b/packages/web/pages/settings/account.tsx index 23b466758..f3cff4b35 100644 --- a/packages/web/pages/settings/account.tsx +++ b/packages/web/pages/settings/account.tsx @@ -1,23 +1,22 @@ import { useCallback, useEffect, useMemo, useState } from 'react' -import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' -import { applyStoredTheme } from '../../lib/themeUpdater' - -import { StyledText } from '../../components/elements/StyledText' -import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery' -import { SettingsLayout } from '../../components/templates/SettingsLayout' import { Toaster } from 'react-hot-toast' +import { Button } from '../../components/elements/Button' import { Box, SpanBox, VStack, } from '../../components/elements/LayoutPrimitives' -import { Button } from '../../components/elements/Button' -import { useValidateUsernameQuery } from '../../lib/networking/queries/useValidateUsernameQuery' +import { StyledText } from '../../components/elements/StyledText' +import { SettingsLayout } from '../../components/templates/SettingsLayout' +import { styled } from '../../components/tokens/stitches.config' +import { updateEmailMutation } from '../../lib/networking/mutations/updateEmailMutation' import { updateUserMutation } from '../../lib/networking/mutations/updateUserMutation' import { updateUserProfileMutation } from '../../lib/networking/mutations/updateUserProfileMutation' -import { styled, theme } from '../../components/tokens/stitches.config' -import { ProgressBar } from '../../components/elements/ProgressBar' import { useGetLibraryItemsQuery } from '../../lib/networking/queries/useGetLibraryItemsQuery' +import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery' +import { useValidateUsernameQuery } from '../../lib/networking/queries/useValidateUsernameQuery' +import { applyStoredTheme } from '../../lib/themeUpdater' +import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' const StyledLabel = styled('label', { fontWeight: 600, @@ -49,6 +48,9 @@ export default function Account(): JSX.Element { const [username, setUsername] = useState('') const [nameUpdating, setNameUpdating] = useState(false) const [usernameUpdating, setUsernameUpdating] = useState(false) + const [email, setEmail] = useState('') + const [emailUpdating, setEmailUpdating] = useState(false) + const [source, setSource] = useState('') const [debouncedUsername, setDebouncedUsername] = useState('') const { usernameErrorMessage, isLoading: isUsernameValidationLoading } = @@ -96,6 +98,18 @@ export default function Account(): JSX.Element { } }, [viewerData?.me?.name]) + useEffect(() => { + if (viewerData?.me?.email) { + setEmail(viewerData?.me?.email) + } + }, [viewerData?.me?.email]) + + useEffect(() => { + if (viewerData?.me?.source) { + setSource(viewerData?.me?.source) + } + }, [viewerData?.me?.source]) + const handleUsernameChange = useCallback( (event: React.ChangeEvent): void => { setUsername(event.target.value) @@ -154,6 +168,24 @@ export default function Account(): JSX.Element { viewerData?.me, ]) + const updateEmail = useCallback(() => { + setEmailUpdating(true) + ;(async () => { + const response = await updateEmailMutation({ email }) + if (response) { + setEmail(response.email) + if (response.verificationEmailSent) { + showSuccessToast('Verification email sent') + } else { + showSuccessToast('Email updated') + } + } else { + showErrorToast('Error updating email') + } + setEmailUpdating(false) + })() + }, [email]) + applyStoredTheme(false) return ( @@ -281,6 +313,50 @@ export default function Account(): JSX.Element { + +
{ + updateEmail() + event.preventDefault() + }} + > + Email + { + setEmail(event.target.value) + event.preventDefault() + }} + /> + + Your email is used for account recovery and notifications. + + {source == 'EMAIL' ? ( + + ) : ( + + + You are currently logged in with a social account. To + convert to an email login, please click the button below. + + + + )} + +
+ {/*