diff --git a/frontend/apps/remark42/app/common/api.ts b/frontend/apps/remark42/app/common/api.ts index 1d049787..2690e8a4 100644 --- a/frontend/apps/remark42/app/common/api.ts +++ b/frontend/apps/remark42/app/common/api.ts @@ -1,6 +1,16 @@ import { siteId, url } from './settings'; import { BASE_URL, API_BASE } from './constants'; -import { Config, Comment, Tree, User, BlockedUser, Sorting, BlockTTL, Image } from './types'; +import { + Config, + Comment, + Tree, + User, + BlockedUser, + Sorting, + BlockTTL, + Image, + EmailSubVerificationStatus, +} from './types'; import { apiFetcher, adminFetcher } from './fetcher'; /* API methods */ @@ -72,8 +82,8 @@ export const uploadImage = (image: File): Promise => { * Start process of email subscription to updates * @param emailAddress email for subscription */ -export const emailVerificationForSubscribe = (emailAddress: string) => - apiFetcher.post('/email/subscribe', {}, { address: emailAddress }); +export const emailVerificationForSubscribe = (emailAddress: string): Promise => + apiFetcher.post('/email/subscribe', {}, { address: emailAddress, autoConfirm: true }); /** * Confirmation of email subscription to updates diff --git a/frontend/apps/remark42/app/common/types.ts b/frontend/apps/remark42/app/common/types.ts index 9e80a827..3d02ee56 100644 --- a/frontend/apps/remark42/app/common/types.ts +++ b/frontend/apps/remark42/app/common/types.ts @@ -156,3 +156,8 @@ export interface ApiError { /** in-depth explanation */ error: string; } + +export interface EmailSubVerificationStatus { + updated: boolean; + address: string; +} diff --git a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx index d5bc5cc9..8aa999ea 100644 --- a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx +++ b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx @@ -18,6 +18,7 @@ import { persistEmail } from 'components/auth/auth.utils'; import enMessages from 'locales/en.json'; import { SubscribeByEmail, SubscribeByEmailForm } from '.'; +import { RequestError } from '../../../utils/errorUtils'; const emailVerificationForSubscribeMock = emailVerificationForSubscribe as unknown as jest.Mock< ReturnType @@ -29,6 +30,8 @@ const unsubscribeFromEmailUpdatesMock = unsubscribeFromEmailUpdates as unknown a ReturnType >; +emailVerificationForSubscribeMock.mockImplementation((email) => Promise.resolve({ address: email, updated: false })); + const initialStore = { user, theme: 'light', @@ -104,7 +107,7 @@ describe('', () => { expect(wrapper.text().startsWith('You are subscribed on updates by email')).toBe(true); }); - it('should pass throw subscribe process', async () => { + it('should pass through subscribe process', async () => { const wrapper = createWrapper(); const input = wrapper.find('input'); @@ -138,6 +141,47 @@ describe('', () => { expect(wrapper.find(Button).text()).toEqual('Unsubscribe'); }); + it('should handle http error 409: already subscribed', async () => { + emailVerificationForSubscribeMock.mockImplementationOnce(() => Promise.reject(new RequestError('', 409))); + + const wrapper = createWrapper(); + + const input = wrapper.find('input'); + const form = wrapper.find('form'); + + input.getDOMNode().value = 'some@email.com'; + input.simulate('input'); + form.simulate('submit'); + + await sleep(); + wrapper.update(); + + expect(wrapper.text().startsWith('You are subscribed on updates by email')).toBe(true); + }); + + it('should pass through subscribe process without confirmation', async () => { + emailVerificationForSubscribeMock.mockImplementationOnce((email) => + Promise.resolve({ address: email, updated: true }) + ); + + const wrapper = createWrapper(); + + const input = wrapper.find('input'); + const form = wrapper.find('form'); + + input.getDOMNode().value = 'some@email.com'; + input.simulate('input'); + form.simulate('submit'); + + expect(emailVerificationForSubscribeMock).toHaveBeenCalledWith('some@email.com'); + + await sleep(); + wrapper.update(); + + expect(wrapper.text().startsWith('You have been subscribed on updates by email')).toBe(true); + expect(wrapper.find(Button).text()).toEqual('Unsubscribe'); + }); + it('should fill in email from local storage', async () => { const expected = 'someone@email.com'; persistEmail(expected); diff --git a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx index 38de6db5..ad26b500 100644 --- a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx +++ b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx @@ -8,7 +8,7 @@ import { User } from 'common/types'; import { StoreState } from 'store'; import { setUserSubscribed } from 'store/user/actions'; import { sleep } from 'utils/sleep'; -import { extractErrorMessageFromResponse } from 'utils/errorUtils'; +import { extractErrorMessageFromResponse, RequestError } from 'utils/errorUtils'; import { useTheme } from 'hooks/useTheme'; import { getHandleClickProps } from 'common/accessibility'; import { emailVerificationForSubscribe, emailConfirmationForSubscribe, unsubscribeFromEmailUpdates } from 'common/api'; @@ -120,7 +120,7 @@ export const SubscribeByEmailForm: FunctionComponent = () => { const subscribed = useSelector(({ user }) => user === null ? false : Boolean(user.email_subscription) ); - const previousStep = useRef(null); + const justSubscribed = useRef(false); const [step, setStep] = useState(subscribed ? Step.Subscribed : Step.Email); @@ -136,16 +136,31 @@ export const SubscribeByEmailForm: FunctionComponent = () => { setError(null); try { + let emailVerificationResponse; + switch (step) { case Step.Email: - await emailVerificationForSubscribe(emailAddress); + try { + emailVerificationResponse = await emailVerificationForSubscribe(emailAddress); + justSubscribed.current = true; + } catch (e) { + if ((e as RequestError).code !== 409) { + throw e; + } + emailVerificationResponse = { address: emailAddress, updated: true }; + } + if (emailVerificationResponse.updated) { + dispatch(setUserSubscribed(true)); + setStep(Step.Subscribed); + break; + } setToken(''); setStep(Step.Token); break; case Step.Token: await emailConfirmationForSubscribe(currentToken); dispatch(setUserSubscribed(true)); - previousStep.current = Step.Token; + justSubscribed.current = true; setStep(Step.Subscribed); break; default: @@ -210,7 +225,6 @@ export const SubscribeByEmailForm: FunctionComponent = () => { try { await unsubscribeFromEmailUpdates(); dispatch(setUserSubscribed(false)); - previousStep.current = Step.Subscribed; setStep(Step.Unsubscribed); } catch (e) { // @ts-ignore @@ -229,10 +243,9 @@ export const SubscribeByEmailForm: FunctionComponent = () => { } if (step === Step.Subscribed) { - const text = - previousStep.current === Step.Token - ? intl.formatMessage(messages.haveSubscribed) - : intl.formatMessage(messages.subscribed); + const text = justSubscribed.current + ? intl.formatMessage(messages.haveSubscribed) + : intl.formatMessage(messages.subscribed); return (
diff --git a/frontend/apps/remark42/app/utils/errorUtils.ts b/frontend/apps/remark42/app/utils/errorUtils.ts index 62ed3e0c..df17c922 100644 --- a/frontend/apps/remark42/app/utils/errorUtils.ts +++ b/frontend/apps/remark42/app/utils/errorUtils.ts @@ -97,6 +97,10 @@ export const errorMessages = defineMessages({ id: 'errors.forbidden', defaultMessage: 'Forbidden.', }, + 409: { + id: 'errors.conflict', + defaultMessage: 'Conflict.', + }, 429: { id: 'errors.to-many-request', defaultMessage: 'You have reached maximum request limit.',