add update email form on account page

This commit is contained in:
Hongbo Wu 2023-10-02 22:21:50 +08:00
parent c40e64773a
commit ee50529f95
10 changed files with 269 additions and 114 deletions

View file

@ -252,28 +252,6 @@ export enum ContentReader {
Web = 'WEB'
}
export type ConvertToEmailError = {
__typename?: 'ConvertToEmailError';
errorCodes: Array<ConvertToEmailErrorCode>;
};
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<CreateArticleErrorCode>;
@ -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<UpdateEmailErrorCode>;
};
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<Scalars['Boolean']>;
};
export type UpdateFilterError = {
__typename?: 'UpdateFilterError';
errorCodes: Array<UpdateFilterErrorCode>;
@ -3239,6 +3240,7 @@ export enum UploadImportFileType {
export type User = {
__typename?: 'User';
email?: Maybe<Scalars['String']>;
followersCount?: Maybe<Scalars['Int']>;
friendsCount?: Maybe<Scalars['Int']>;
id: Scalars['ID'];
@ -3252,6 +3254,7 @@ export type User = {
sharedArticlesCount?: Maybe<Scalars['Int']>;
sharedHighlightsCount?: Maybe<Scalars['Int']>;
sharedNotesCount?: Maybe<Scalars['Int']>;
source?: Maybe<Scalars['String']>;
viewerIsFollowing?: Maybe<Scalars['Boolean']>;
};
@ -3470,11 +3473,6 @@ export type ResolversTypes = {
BulkActionSuccess: ResolverTypeWrapper<BulkActionSuccess>;
BulkActionType: BulkActionType;
ContentReader: ContentReader;
ConvertToEmailError: ResolverTypeWrapper<ConvertToEmailError>;
ConvertToEmailErrorCode: ConvertToEmailErrorCode;
ConvertToEmailInput: ConvertToEmailInput;
ConvertToEmailResult: ResolversTypes['ConvertToEmailError'] | ResolversTypes['ConvertToEmailSuccess'];
ConvertToEmailSuccess: ResolverTypeWrapper<ConvertToEmailSuccess>;
CreateArticleError: ResolverTypeWrapper<CreateArticleError>;
CreateArticleErrorCode: CreateArticleErrorCode;
CreateArticleInput: CreateArticleInput;
@ -3846,6 +3844,11 @@ export type ResolversTypes = {
UnsubscribeErrorCode: UnsubscribeErrorCode;
UnsubscribeResult: ResolversTypes['UnsubscribeError'] | ResolversTypes['UnsubscribeSuccess'];
UnsubscribeSuccess: ResolverTypeWrapper<UnsubscribeSuccess>;
UpdateEmailError: ResolverTypeWrapper<UpdateEmailError>;
UpdateEmailErrorCode: UpdateEmailErrorCode;
UpdateEmailInput: UpdateEmailInput;
UpdateEmailResult: ResolversTypes['UpdateEmailError'] | ResolversTypes['UpdateEmailSuccess'];
UpdateEmailSuccess: ResolverTypeWrapper<UpdateEmailSuccess>;
UpdateFilterError: ResolverTypeWrapper<UpdateFilterError>;
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<ContextType = ResolverContext, ParentType
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ConvertToEmailErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['ConvertToEmailError'] = ResolversParentTypes['ConvertToEmailError']> = {
errorCodes?: Resolver<Array<ResolversTypes['ConvertToEmailErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ConvertToEmailResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['ConvertToEmailResult'] = ResolversParentTypes['ConvertToEmailResult']> = {
__resolveType: TypeResolveFn<'ConvertToEmailError' | 'ConvertToEmailSuccess', ParentType, ContextType>;
};
export type ConvertToEmailSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['ConvertToEmailSuccess'] = ResolversParentTypes['ConvertToEmailSuccess']> = {
success?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type CreateArticleErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['CreateArticleError'] = ResolversParentTypes['CreateArticleError']> = {
errorCodes?: Resolver<Array<ResolversTypes['CreateArticleErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -5242,7 +5231,6 @@ export type MoveLabelSuccessResolvers<ContextType = ResolverContext, ParentType
export type MutationResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Mutation'] = ResolversParentTypes['Mutation']> = {
addPopularRead?: Resolver<ResolversTypes['AddPopularReadResult'], ParentType, ContextType, RequireFields<MutationAddPopularReadArgs, 'name'>>;
bulkAction?: Resolver<ResolversTypes['BulkActionResult'], ParentType, ContextType, RequireFields<MutationBulkActionArgs, 'action' | 'query'>>;
convertToEmail?: Resolver<ResolversTypes['ConvertToEmailResult'], ParentType, ContextType, RequireFields<MutationConvertToEmailArgs, 'input'>>;
createArticle?: Resolver<ResolversTypes['CreateArticleResult'], ParentType, ContextType, RequireFields<MutationCreateArticleArgs, 'input'>>;
createArticleSavingRequest?: Resolver<ResolversTypes['CreateArticleSavingRequestResult'], ParentType, ContextType, RequireFields<MutationCreateArticleSavingRequestArgs, 'input'>>;
createGroup?: Resolver<ResolversTypes['CreateGroupResult'], ParentType, ContextType, RequireFields<MutationCreateGroupArgs, 'input'>>;
@ -5299,6 +5287,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
setWebhook?: Resolver<ResolversTypes['SetWebhookResult'], ParentType, ContextType, RequireFields<MutationSetWebhookArgs, 'input'>>;
subscribe?: Resolver<ResolversTypes['SubscribeResult'], ParentType, ContextType, RequireFields<MutationSubscribeArgs, 'input'>>;
unsubscribe?: Resolver<ResolversTypes['UnsubscribeResult'], ParentType, ContextType, RequireFields<MutationUnsubscribeArgs, 'name'>>;
updateEmail?: Resolver<ResolversTypes['UpdateEmailResult'], ParentType, ContextType, RequireFields<MutationUpdateEmailArgs, 'input'>>;
updateFilter?: Resolver<ResolversTypes['UpdateFilterResult'], ParentType, ContextType, RequireFields<MutationUpdateFilterArgs, 'input'>>;
updateHighlight?: Resolver<ResolversTypes['UpdateHighlightResult'], ParentType, ContextType, RequireFields<MutationUpdateHighlightArgs, 'input'>>;
updateHighlightReply?: Resolver<ResolversTypes['UpdateHighlightReplyResult'], ParentType, ContextType, RequireFields<MutationUpdateHighlightReplyArgs, 'input'>>;
@ -6007,6 +5996,21 @@ export type UnsubscribeSuccessResolvers<ContextType = ResolverContext, ParentTyp
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type UpdateEmailErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateEmailError'] = ResolversParentTypes['UpdateEmailError']> = {
errorCodes?: Resolver<Array<ResolversTypes['UpdateEmailErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type UpdateEmailResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateEmailResult'] = ResolversParentTypes['UpdateEmailResult']> = {
__resolveType: TypeResolveFn<'UpdateEmailError' | 'UpdateEmailSuccess', ParentType, ContextType>;
};
export type UpdateEmailSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateEmailSuccess'] = ResolversParentTypes['UpdateEmailSuccess']> = {
email?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
verificationEmailSent?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type UpdateFilterErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateFilterError'] = ResolversParentTypes['UpdateFilterError']> = {
errorCodes?: Resolver<Array<ResolversTypes['UpdateFilterErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -6209,6 +6213,7 @@ export type UploadImportFileSuccessResolvers<ContextType = ResolverContext, Pare
};
export type UserResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User']> = {
email?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
followersCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
friendsCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
@ -6221,6 +6226,7 @@ export type UserResolvers<ContextType = ResolverContext, ParentType extends Reso
sharedArticlesCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
sharedHighlightsCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
sharedNotesCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
source?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
viewerIsFollowing?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -6334,9 +6340,6 @@ export type Resolvers<ContextType = ResolverContext> = {
BulkActionError?: BulkActionErrorResolvers<ContextType>;
BulkActionResult?: BulkActionResultResolvers<ContextType>;
BulkActionSuccess?: BulkActionSuccessResolvers<ContextType>;
ConvertToEmailError?: ConvertToEmailErrorResolvers<ContextType>;
ConvertToEmailResult?: ConvertToEmailResultResolvers<ContextType>;
ConvertToEmailSuccess?: ConvertToEmailSuccessResolvers<ContextType>;
CreateArticleError?: CreateArticleErrorResolvers<ContextType>;
CreateArticleResult?: CreateArticleResultResolvers<ContextType>;
CreateArticleSavingRequestError?: CreateArticleSavingRequestErrorResolvers<ContextType>;
@ -6583,6 +6586,9 @@ export type Resolvers<ContextType = ResolverContext> = {
UnsubscribeError?: UnsubscribeErrorResolvers<ContextType>;
UnsubscribeResult?: UnsubscribeResultResolvers<ContextType>;
UnsubscribeSuccess?: UnsubscribeSuccessResolvers<ContextType>;
UpdateEmailError?: UpdateEmailErrorResolvers<ContextType>;
UpdateEmailResult?: UpdateEmailResultResolvers<ContextType>;
UpdateEmailSuccess?: UpdateEmailSuccessResolvers<ContextType>;
UpdateFilterError?: UpdateFilterErrorResolvers<ContextType>;
UpdateFilterResult?: UpdateFilterResultResolvers<ContextType>;
UpdateFilterSuccess?: UpdateFilterSuccessResolvers<ContextType>;

View file

@ -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
}

View file

@ -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'),
}

View file

@ -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],
}
}
})

View file

@ -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,
})
}
)

View file

@ -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!

View file

@ -31,7 +31,7 @@ export const sendVerificationEmail = async (user: {
}): Promise<boolean> => {
// 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,

View file

@ -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<UpdateEmailSuccess | undefined> {
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
}
}

View file

@ -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
}
}
`

View file

@ -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<HTMLInputElement>): 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 {
</form>
</VStack>
<VStack
css={{
padding: '24px',
width: '100%',
height: '100%',
bg: '$grayBg',
gap: '5px',
borderRadius: '5px',
}}
>
<form
onSubmit={(event) => {
updateEmail()
event.preventDefault()
}}
>
<StyledLabel>Email</StyledLabel>
<FormInput
type={'text'}
placeholder={'Email'}
value={email}
disabled={emailUpdating}
onChange={(event) => {
setEmail(event.target.value)
event.preventDefault()
}}
/>
<StyledText style="footnote" css={{ mt: '10px', mb: '20px' }}>
Your email is used for account recovery and notifications.
</StyledText>
{source == 'EMAIL' ? (
<Button style="ctaDarkYellow">Update Email</Button>
) : (
<VStack>
<StyledText style="footnote" css={{ mt: '10px', mb: '20px' }}>
You are currently logged in with a social account. To
convert to an email login, please click the button below.
</StyledText>
<Button style="ctaDarkYellow">Convert to email login</Button>
</VStack>
)}
</form>
</VStack>
{/* <VStack
css={{
padding: '24px',