diff --git a/package.json b/package.json index 04b5f7f77..0edab7022 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "adguard-vpn", - "version": "2.1.11", + "version": "2.2.1", "description": "AdGuard VPN", "author": "adguard@adguard.com", "license": "LGPL-3.0", diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 2e2ee4bc7..5759faa82 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -51,7 +51,7 @@ "message": "An error occurred, try again later" }, "global_error_message": { - "message": "An error occurred, please contact us via support@adguard-vpn.com" + "message": "An error occurred, please contact us via %support_email%" }, "about_title": { "message": "About" @@ -939,7 +939,7 @@ "message": "Confirm your email" }, "confirm_email_info": { - "message": "To get free VPN traffic, please confirm your email address. We’ve sent a confirmation link to" + "message": "We’ve sent a confirmation code to %email%" }, "confirm_email_done_title": { "message": "Email address confirmed" @@ -947,11 +947,26 @@ "confirm_email_done_info": { "message": "Thanks for verifying it!" }, - "confirm_email_close_button": { - "message": "Close" + "confirm_email_code_label": { + "message": "Confirmation code" }, - "confirm_email_resend_link_button": { - "message": "Resend link" + "confirm_email_code_placeholder": { + "message": "Contains numbers and letters" + }, + "confirm_email_code_invalid": { + "message": "Please check the code — it looks incorrect" + }, + "confirm_email_resend_code_button_disabled": { + "message": "Send another code (%count%)" + }, + "confirm_email_resend_code_button": { + "message": "Send another code" + }, + "confirm_email_no_auth_id_error_on_auth": { + "message": "Failed to log you in. Please try again or contact us via %support_email%" + }, + "confirm_email_no_auth_id_error_on_register": { + "message": "Failed to sign you up. Please try again or contact us via %support_email%" }, "resend_confirm_registration_link_notification": { "message": "Confirmation link resent" diff --git a/src/background/api/Api.ts b/src/background/api/Api.ts index 8d205e5f8..463194e1b 100644 --- a/src/background/api/Api.ts +++ b/src/background/api/Api.ts @@ -4,6 +4,8 @@ import { notifier } from '../../lib/notifier'; const REQUEST_TIMEOUT_MS = 1000 * 6; // 6 seconds +const HTTP_RESPONSE_STATUS_OK = 200; + interface ConfigInterface { params?: { [key: string]: string; @@ -92,7 +94,17 @@ export class Api implements ApiInterface { return response; } - const responseData = await response.json(); + let responseData; + try { + responseData = await response.json(); + } catch (e) { + // server response may be empty, + // e.g. 'api/2.0/resend_confirmation_code' response is 200 but may be empty, + // that's why response.json() can throw an error + if (response.status !== HTTP_RESPONSE_STATUS_OK) { + throw new CustomError(response.status, JSON.stringify(e)); + } + } return responseData; } catch (e) { if (e instanceof CustomError) { diff --git a/src/background/api/apiTypes.ts b/src/background/api/apiTypes.ts index fb1e973a5..d0d5bf069 100644 --- a/src/background/api/apiTypes.ts +++ b/src/background/api/apiTypes.ts @@ -8,6 +8,11 @@ export type AuthCredentials = { locale: string; clientId: string; appId: string; + + /** + * Optional email confirmation code. + */ + code?: string; }; export type RequestProps = { diff --git a/src/background/api/authApi.ts b/src/background/api/authApi.ts index 9d2c98d34..ad886ffd1 100644 --- a/src/background/api/authApi.ts +++ b/src/background/api/authApi.ts @@ -1,4 +1,5 @@ import { AUTH_CLIENT_ID } from '../config'; +import { appStatus } from '../appStatus'; import { Api } from './Api'; import { fallbackApi } from './fallbackApi'; @@ -11,7 +12,12 @@ class AuthApi extends Api { GET_TOKEN: RequestProps = { path: 'oauth/token', method: 'POST' }; getAccessToken(credentials: AuthCredentials) { - const { username, password, twoFactor } = credentials; + const { + username, + password, + twoFactor, + code, + } = credentials; const { path, method } = this.GET_TOKEN; type Data = { @@ -20,7 +26,9 @@ class AuthApi extends Api { scope: string; grant_type: string; client_id: string; + app_version: string; '2fa_token'?: string; + code?: string; }; const params: Data = { @@ -29,12 +37,17 @@ class AuthApi extends Api { scope: 'trust', grant_type: 'password_2fa', client_id: AUTH_CLIENT_ID, + app_version: appStatus.version, }; if (twoFactor) { params['2fa_token'] = twoFactor; } + if (code) { + params.code = code; + } + return this.makeRequest(path, { params }, method); } @@ -75,6 +88,18 @@ class AuthApi extends Api { }; return this.makeRequest(path, { params }, method); } + + RESEND_CONFIRMATION_CODE: RequestProps = { path: 'api/2.0/resend_confirmation_code', method: 'POST' }; + + resendCode(authId: string) { + const { path, method } = this.RESEND_CONFIRMATION_CODE; + + const params = { + auth_id: authId, + }; + + return this.makeRequest(path, { params }, method); + } } export const authApi = new AuthApi(async () => { diff --git a/src/background/auth/auth.ts b/src/background/auth/auth.ts index 8f582d956..27a100dbf 100644 --- a/src/background/auth/auth.ts +++ b/src/background/auth/auth.ts @@ -8,6 +8,7 @@ import { notifications } from '../notifications'; import { AUTH_CLIENT_ID } from '../config'; import { log } from '../../lib/logger'; import { notifier } from '../../lib/notifier'; +import { SUPPORT_EMAIL } from '../../common/constants'; import { translator } from '../../common/translator'; import { fallbackApi } from '../api/fallbackApi'; // eslint-disable-next-line import/no-cycle @@ -38,6 +39,7 @@ export interface AuthInterface { email: string, appId: string, ): Promise<{ canRegister: string } | { error: string }>; + resendEmailConfirmationCode(authId: string): Promise; getAccessToken(turnOffProxy?: boolean): Promise; init(): Promise; } @@ -254,7 +256,7 @@ class Auth implements AuthInterface { async register( credentials: AuthCredentials, - ): Promise<{ status: string } | { error: string, field?: string }> { + ): Promise<{ status: string } | { error: string, field?: string, status?: string, authId?: string }> { const locale = navigator.language; let accessToken; try { @@ -264,8 +266,18 @@ class Auth implements AuthInterface { clientId: AUTH_CLIENT_ID, }); } catch (e) { - const { error, field } = JSON.parse(e.message); - return { error, field }; + const { + error, + field, + status, + authId, + } = JSON.parse(e.message); + return { + error, + field, + status, + authId, + }; } if (accessToken) { @@ -276,7 +288,8 @@ class Auth implements AuthInterface { return { error: translator.getMessage('global_error_message', { - a: (chunks: string) => `${chunks}`, + support_email: SUPPORT_EMAIL, + a: (chunks: string) => `${chunks}`, }), }; } @@ -297,13 +310,23 @@ class Auth implements AuthInterface { log.error(e.message); return { error: translator.getMessage('global_error_message', { - a: (chunks: string) => `${chunks}`, + support_email: SUPPORT_EMAIL, + a: (chunks: string) => `${chunks}`, }), }; } return response; } + /** + * Uses {@link authProvider} to request a new email confirmation code. + * + * @param authId Auth id received from the server previously. + */ + async resendEmailConfirmationCode(authId: string): Promise { + await authProvider.resendEmailConfirmationCode(authId); + } + async setAccessToken(accessToken: AuthAccessToken): Promise { this.accessTokenData = accessToken; await authService.saveAccessTokenData(accessToken); diff --git a/src/background/authentication/authCache.ts b/src/background/authentication/authCache.ts index feaf00102..e68653b61 100644 --- a/src/background/authentication/authCache.ts +++ b/src/background/authentication/authCache.ts @@ -21,6 +21,7 @@ const AuthCache = (): AuthCacheInterface => { helpUsImprove: null, marketingConsent: null, authError: null, + code: '', }; let authCache = { ...DEFAULTS }; diff --git a/src/background/emailConfirmationService/emailConfirmationService.ts b/src/background/emailConfirmationService/emailConfirmationService.ts new file mode 100644 index 000000000..2049414e2 --- /dev/null +++ b/src/background/emailConfirmationService/emailConfirmationService.ts @@ -0,0 +1,122 @@ +import { BrowserApi, browserApi } from '../browserApi'; +import { RESEND_EMAIL_CONFIRMATION_CODE_DELAY_SEC } from '../../common/constants'; + +/** + * Service for email confirmation. + * + * It is used to store confirmation authId and resend code count down. + */ +class EmailConfirmationService { + browserApi: BrowserApi; + + /** + * Email for which count down is started. + * + * Needed to check if resend code count down should be restarted on popup navigation, + * i.e. if user's email should be confirmed but user navigates back and changes the password — + * count down should not be restarted in this case. + */ + private email: string | null; + + /** + * Value of 'auth_id' field from server response of registration or authorization request + * if user should confirm email. + */ + private confirmationAuthId: string | null; + + private COUNTDOWN_START_KEY = 'resend.code.countdown.start.time'; + + /** + * Timestamp in milliseconds on which count down was started last time. + */ + private countdownStartMs: number | null; + + constructor(providedBrowserApi: BrowserApi) { + this.browserApi = providedBrowserApi; + this.confirmationAuthId = null; + } + + /** + * Returns resend code count down start timestamp in MILLISECONDS. + * + * If there is no value for {@link countdownStartMs}, it will be fetched from storage. + * + * @returns Timestamp in milliseconds on which count down was started last time + * or null if count down was not started. + */ + getResentCodeCountdownStartFromStorage = async (): Promise => { + if (this.countdownStartMs) { + return this.countdownStartMs; + } + this.countdownStartMs = await this.browserApi.storage.get(this.COUNTDOWN_START_KEY) || null; + return this.countdownStartMs; + }; + + /** + * Sets resend code count down start timestamp in MILLISECONDS in storage. + * + * @param countdownStart Timestamp in milliseconds on which count down was started last time. + */ + setResentCodeCountdownStartToStorage = async (countdownStart: number | null): Promise => { + this.countdownStartMs = countdownStart; + await this.browserApi.storage.set(this.COUNTDOWN_START_KEY, countdownStart); + }; + + /** + * Resets count down and starts it again if the timer has not been started yet. + * + * @param email User email for which count down should be restarted. + * If provided and is the same as previous, count down is not restarted. + * If not provided, count down is restarted anyway. + */ + public restartCountdown(email?: string): void { + // if email is provided, check if it is the same as previous + if (typeof email !== 'undefined') { + if (this.email === email) { + // do not restart count down if email is the same + return; + } + // update email value + this.email = email; + } + + this.setResentCodeCountdownStartToStorage(Date.now()); + } + + /** + * Returns number of SECONDS left until user can request another code. + * + * @returns Number of seconds left or null if count down was not started or already finished. + */ + public async getCodeCountdown(): Promise { + const countdownStartMs = await this.getResentCodeCountdownStartFromStorage(); + if (!countdownStartMs) { + return null; + } + + // eslint-disable-next-line max-len + const countdownSec = RESEND_EMAIL_CONFIRMATION_CODE_DELAY_SEC - Math.round((Date.now() - countdownStartMs) / 1000); + return countdownSec > 0 + ? countdownSec + : null; + } + + /** + * Sets authId which is needed for email confirmation, + * i.e. for requesting another code. + * + * @param authId AuthId. + */ + public setAuthId(authId: string) { + this.confirmationAuthId = authId; + } + + /** + * Returns authId for confirmation. + */ + public get authId(): string | null { + return this.confirmationAuthId; + } +} + +export const emailConfirmationService = new EmailConfirmationService(browserApi); diff --git a/src/background/emailConfirmationService/index.ts b/src/background/emailConfirmationService/index.ts new file mode 100644 index 000000000..aee7b7369 --- /dev/null +++ b/src/background/emailConfirmationService/index.ts @@ -0,0 +1 @@ +export { emailConfirmationService } from './emailConfirmationService'; diff --git a/src/background/messaging/messaging.ts b/src/background/messaging/messaging.ts index 25e64f7ec..afdea0f3a 100644 --- a/src/background/messaging/messaging.ts +++ b/src/background/messaging/messaging.ts @@ -26,6 +26,7 @@ import { ExclusionsData } from '../../common/exclusionsConstants'; import { rateModal } from '../rateModal'; import { dns } from '../dns'; import { hintPopup } from '../hintPopup'; +import { emailConfirmationService } from '../emailConfirmationService'; import { CUSTOM_DNS_ANCHOR_NAME } from '../../common/constants'; interface Message { @@ -183,6 +184,7 @@ const messagesHandler = async (message: Message, sender: Runtime.MessageSender) } case MessageType.AUTHENTICATE_USER: { const { credentials } = data; + emailConfirmationService.restartCountdown(credentials.username); return auth.authenticate(credentials); } case MessageType.UPDATE_AUTH_CACHE: { @@ -253,6 +255,7 @@ const messagesHandler = async (message: Message, sender: Runtime.MessageSender) } case MessageType.REGISTER_USER: { const appId = await credentials.getAppId(); + emailConfirmationService.restartCountdown(data.credentials.username); return auth.register({ ...data.credentials, appId }); } case MessageType.IS_AUTHENTICATED: { @@ -401,6 +404,27 @@ const messagesHandler = async (message: Message, sender: Runtime.MessageSender) const accessToken = await auth.getAccessToken(); return accountProvider.resendConfirmRegistrationLink(accessToken, displayNotification); } + case MessageType.SET_EMAIL_CONFIRMATION_AUTH_ID: { + const { authId } = data; + emailConfirmationService.setAuthId(authId); + break; + } + case MessageType.RESEND_EMAIL_CONFIRMATION_CODE: { + emailConfirmationService.restartCountdown(); + + const { authId } = emailConfirmationService; + if (!authId) { + log.error('Value authId was not set in the emailConfirmationService'); + break; + } + + await auth.resendEmailConfirmationCode(authId); + break; + } + case MessageType.GET_RESEND_CODE_COUNTDOWN: { + const countdown = await emailConfirmationService.getCodeCountdown(); + return countdown; + } case MessageType.RESTORE_CUSTOM_DNS_SERVERS_DATA: { return dns.restoreCustomDnsServersData(); } diff --git a/src/background/providers/authProvider.ts b/src/background/providers/authProvider.ts index 224680d7d..3e74c0179 100644 --- a/src/background/providers/authProvider.ts +++ b/src/background/providers/authProvider.ts @@ -1,3 +1,9 @@ +import { + BAD_CREDENTIALS_CODE, + REQUIRED_2FA_CODE, + REQUIRED_EMAIL_CONFIRMATION_CODE, + SUPPORT_EMAIL, +} from '../../common/constants'; import { authApi } from '../api'; import { translator } from '../../common/translator'; import { FORWARDER_DOMAIN } from '../config'; @@ -33,17 +39,29 @@ const accessTokenModel = { }, }; +/** + * Uses {@link authApi} to request access token. + * + * @param credentials Credentials for authentication. + * + * @returns Auth access token data. + * @throws Error if could not get access token. + */ const getAccessToken = async (credentials: AuthCredentials): Promise => { const TOO_MANY_REQUESTS_CODE = 429; + const INVALID_EMAIL_CONFIRMATION_CODE = 'confirmation_code_invalid'; let accessTokenData; const errorsMap: ErrorsMap = { - '2fa_required': translator.getMessage('authentication_error_2fa_required'), + [REQUIRED_2FA_CODE]: translator.getMessage('authentication_error_2fa_required'), '2fa_invalid': translator.getMessage('authentication_error_2fa_invalid'), account_disabled: translator.getMessage('authentication_error_account_disabled'), account_locked: translator.getMessage('authentication_error_account_locked'), - bad_credentials: translator.getMessage('authentication_error_wrong_credentials'), + [BAD_CREDENTIALS_CODE]: translator.getMessage('authentication_error_wrong_credentials'), [TOO_MANY_REQUESTS_CODE]: translator.getMessage('authentication_too_many_requests'), + // the value is not shown to users, so it is not translated + [REQUIRED_EMAIL_CONFIRMATION_CODE]: REQUIRED_EMAIL_CONFIRMATION_CODE, + [INVALID_EMAIL_CONFIRMATION_CODE]: translator.getMessage('confirm_email_code_invalid'), default: translator.getMessage('authentication_error_default'), }; @@ -51,24 +69,41 @@ const getAccessToken = async (credentials: AuthCredentials): Promise `${chunks}`, + }); + throw new Error(JSON.stringify({ status: errorCode, error })); + } + + throw new Error(JSON.stringify({ status: errorCode, authId })); + } - if (errorCode === '2fa_required') { + if (errorCode === REQUIRED_2FA_CODE) { throw new Error(JSON.stringify({ status: errorCode })); } const error = errorsMap[errorCode] || errorsMap[errorStatusCode] || errorsMap.default; - throw new Error(JSON.stringify({ error })); + throw new Error(JSON.stringify({ error, status: errorCode })); } return accessTokenModel.fromRemoteToLocal(accessTokenData); @@ -87,16 +122,20 @@ const register = async (credentials: AuthCredentials) => { a: (chunks: string[]) => (`${chunks}`), }), 'validation.unique_constraint': translator.getMessage('registration_error_unique_constraint'), + // the value is not shown to users, so it is not translated + [REQUIRED_EMAIL_CONFIRMATION_CODE]: REQUIRED_EMAIL_CONFIRMATION_CODE, default: translator.getMessage('registration_error_default'), }; let accessTokenData; try { accessTokenData = await authApi.register(credentials); + // send a request to 'oauth/token' to check if email confirmation is required + accessTokenData = await authApi.getAccessToken(credentials); } catch (e) { - let errorMessage; + let errorData; try { - errorMessage = JSON.parse(e.message); + errorData = JSON.parse(e.message); } catch (e) { // if was unable to parse error message, e.g. network is disabled throw new Error(JSON.stringify({ error: errorsMap.default })); @@ -105,13 +144,30 @@ const register = async (credentials: AuthCredentials) => { const { error_code: errorCode, field, + auth_id: authId, }: { error_code: keyof typeof errorsMap, field: keyof typeof fieldsMap - } = errorMessage; + auth_id: string, + } = errorData; + + if (errorCode === REQUIRED_EMAIL_CONFIRMATION_CODE) { + // authId is required to request another code + // so error should be shown in the popup if there is no authId + if (!authId) { + const error = translator.getMessage('confirm_email_no_auth_id_error_on_register', { + support_email: SUPPORT_EMAIL, + a: (chunks: string) => `${chunks}`, + }); + throw new Error(JSON.stringify({ status: errorCode, error })); + } + + throw new Error(JSON.stringify({ status: errorCode, authId })); + } const extensionField = fieldsMap[field] || field; const error = errorsMap[errorCode] || errorsMap.default; + throw new Error(JSON.stringify({ error, field: extensionField })); } @@ -123,8 +179,18 @@ const userLookup = async (email: string, appId: string) => { return { canRegister }; }; +/** + * Uses {@link authApi} to request another email confirmation code. + * + * @param authId Auth id. + */ +const resendEmailConfirmationCode = async (authId: string) => { + await authApi.resendCode(authId); +}; + export const authProvider = { getAccessToken, register, userLookup, + resendEmailConfirmationCode, }; diff --git a/src/background/providers/vpnProvider.ts b/src/background/providers/vpnProvider.ts index b9ca602d1..a8a81d493 100644 --- a/src/background/providers/vpnProvider.ts +++ b/src/background/providers/vpnProvider.ts @@ -212,16 +212,9 @@ const getVpnExtensionInfo = async ( max_uploaded_bytes: maxUploadedBytes, renewal_traffic_date: renewalTrafficDate, max_devices_count: maxDevicesCount, - // Temporary disable email confirmation - // TODO: enable when backend will be ready - // email_confirmation_required: emailConfirmationRequired, + email_confirmation_required: emailConfirmationRequired, } = info; - // Temporary hardcode emailConfirmationRequired to false - // to don't show email confirmation modal - // TODO: remove hardcode and handle email_confirmation_required from backend - const emailConfirmationRequired = false; - return { bandwidthFreeMbits, premiumPromoPage, diff --git a/src/common/constants.ts b/src/common/constants.ts index 64c02e229..6cb75079d 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -1 +1,12 @@ export const CUSTOM_DNS_ANCHOR_NAME = 'custom-dns'; + +export const SUPPORT_EMAIL = 'support@adguard-vpn.com'; + +export const REQUIRED_2FA_CODE = '2fa_required'; +export const REQUIRED_EMAIL_CONFIRMATION_CODE = 'confirmation_code_required'; +export const BAD_CREDENTIALS_CODE = 'bad_credentials'; + +/** + * Delay in seconds before user can resend email confirmation code. + */ +export const RESEND_EMAIL_CONFIRMATION_CODE_DELAY_SEC = 60; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index b3aa55f9a..7fce0c733 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -103,6 +103,9 @@ export enum MessageType { EDIT_CUSTOM_DNS_SERVER = 'edit.custom.dns.server', REMOVE_CUSTOM_DNS_SERVER = 'remove.custom.dns.server', RESEND_CONFIRM_REGISTRATION_LINK = 'resend.confirm.registration.link', + SET_EMAIL_CONFIRMATION_AUTH_ID = 'set.email.confirmation.auth.id', + RESEND_EMAIL_CONFIRMATION_CODE = 'resend.email.confirmation.code', + GET_RESEND_CODE_COUNTDOWN = 'get.resend.code.countdown', RESTORE_CUSTOM_DNS_SERVERS_DATA = 'restore.custom.dns.servers.data', SET_HINT_POPUP_VIEWED = 'set.hint.popup.viewed', diff --git a/src/lib/messenger.ts b/src/lib/messenger.ts index aeb003179..9b9944533 100644 --- a/src/lib/messenger.ts +++ b/src/lib/messenger.ts @@ -433,6 +433,35 @@ class Messenger { return this.sendMessage(type, { displayNotification }); } + /** + * Sends a message to the background page to store an authId for email confirmation. + * It is needed for another code request. + * + * @param authId AuthId for email confirmation. + */ + setEmailConfirmationAuthId(authId: string) { + const type = MessageType.SET_EMAIL_CONFIRMATION_AUTH_ID; + return this.sendMessage(type, { authId }); + } + + /** + * Sends a message to the background page to ask for a new email confirmation code. + */ + resendEmailConfirmationCode() { + const type = MessageType.RESEND_EMAIL_CONFIRMATION_CODE; + return this.sendMessage(type); + } + + /** + * Sends a message to the background page to get the resend code count down. + * + * @returns Number of seconds left before the user can request a new code. + */ + async getResendCodeCountdown() { + const type = MessageType.GET_RESEND_CODE_COUNTDOWN; + return this.sendMessage(type); + } + /** * Returns logs from the background page */ diff --git a/src/popup/components/App/App.tsx b/src/popup/components/App/App.tsx index 43882e636..6c1b94906 100644 --- a/src/popup/components/App/App.tsx +++ b/src/popup/components/App/App.tsx @@ -18,7 +18,7 @@ import { Icons } from '../ui/Icons'; import { CurrentEndpoint } from '../Settings/CurrentEndpoint'; import { ExclusionsScreen } from '../Settings/ExclusionsScreen'; import { rootStore } from '../../stores'; -import { RequestStatus } from '../../stores/consts'; +import { RequestStatus } from '../../stores/constants'; import { log } from '../../../lib/logger'; import { messenger } from '../../../lib/messenger'; import { notifier, NotifierType } from '../../../lib/notifier'; @@ -30,7 +30,6 @@ import { Newsletter } from '../Authentication/Newsletter'; import { UpgradeScreen } from '../Authentication/UpgradeScreen'; import { DotsLoader } from '../../../common/components/DotsLoader'; import { ReviewPopup } from '../ReviewPopup'; -import { ConfirmEmailModal, ConfirmEmailNotice } from '../ConfirmEmail'; import { ServerErrorPopup } from '../ServerErrorPopup'; import { VpnBlockedError } from '../VpnBlockedError'; import { HostPermissionsError } from '../HostPermissionsError'; @@ -279,7 +278,6 @@ export const App = observer(() => { ? : ( <> -
{premiumPromoEnabled ? ( @@ -292,7 +290,6 @@ export const App = observer(() => { )} - diff --git a/src/popup/components/Authentication/Authentication.tsx b/src/popup/components/Authentication/Authentication.tsx index f1747292b..13c63fa61 100644 --- a/src/popup/components/Authentication/Authentication.tsx +++ b/src/popup/components/Authentication/Authentication.tsx @@ -10,6 +10,7 @@ import { TwoFactorForm } from './TwoFactorForm'; import { BackButton } from './BackButton'; import { PolicyAgreement } from './PolicyAgreement'; import { ScreenShot } from './ScreenShot'; +import { ConfirmEmail } from './ConfirmEmail'; import './auth.pcss'; @@ -23,6 +24,7 @@ export const Authentication = observer(() => { signIn: , registration: , twoFactor: , + confirmEmail: , }; return titleMaps[step] || titleMaps.authorization; }; @@ -47,6 +49,9 @@ export const Authentication = observer(() => { case authStore.STEPS.POLICY_AGREEMENT: { return ; } + case authStore.STEPS.CONFIRM_EMAIL: { + return ; + } default: { return ; } diff --git a/src/popup/components/Authentication/Authorization/authorization.pcss b/src/popup/components/Authentication/Authorization/authorization.pcss index c95d20fdd..48cb72a26 100644 --- a/src/popup/components/Authentication/Authorization/authorization.pcss +++ b/src/popup/components/Authentication/Authorization/authorization.pcss @@ -1,7 +1,8 @@ .authorization { &__title { - font-size: 2.4rem; - font-weight: 500; + font-size: 24px; + font-weight: 700; + line-height: 30px; margin-bottom: 6px; text-align: center; } @@ -42,6 +43,6 @@ width: 80px; height: 2px; background-color: var(--grayd8); - margin: 0 auto 30px; + margin: 6px auto 30px; } } diff --git a/src/popup/components/Authentication/BackButton.tsx b/src/popup/components/Authentication/BackButton.tsx index 88bc4cc5a..245121336 100644 --- a/src/popup/components/Authentication/BackButton.tsx +++ b/src/popup/components/Authentication/BackButton.tsx @@ -5,9 +5,24 @@ import { rootStore } from '../../stores'; export const BackButton = () => { const { authStore } = useContext(rootStore); + const { prevSteps } = authStore; + const handleBackClick = async () => { await authStore.resetPasswords(); - await authStore.showAuthorizationScreen(); + await authStore.resetCode(); + + if (prevSteps.length === 0) { + await authStore.showAuthorizationScreen(); + } + + const prevStep = prevSteps.pop(); + if (!prevStep) { + await authStore.showAuthorizationScreen(); + return; + } + + authStore.resetRequestProcessionState(); + authStore.switchStep(prevStep); }; return ( diff --git a/src/popup/components/Authentication/ConfirmEmail/ConfirmEmail.tsx b/src/popup/components/Authentication/ConfirmEmail/ConfirmEmail.tsx new file mode 100644 index 000000000..0982e2b25 --- /dev/null +++ b/src/popup/components/Authentication/ConfirmEmail/ConfirmEmail.tsx @@ -0,0 +1,109 @@ +import React, { useContext } from 'react'; +import { observer } from 'mobx-react'; +import ReactHtmlParser from 'react-html-parser'; + +import classnames from 'classnames'; + +import { translator } from '../../../../common/translator'; +import { reactTranslator } from '../../../../common/reactTranslator'; +import { rootStore } from '../../../stores'; +import { RequestStatus } from '../../../stores/constants'; +import { Submit } from '../Submit'; +import { InputField } from '../InputField'; + +export const ConfirmEmail = observer(() => { + const { authStore } = useContext(rootStore); + const { + resendEmailConfirmationCode, + resendCodeCountdown, + requestProcessState, + credentials, + } = authStore; + + const { code } = credentials; + + const submitHandler = async (e: React.FormEvent) => { + e.preventDefault(); + await authStore.authenticate(); + }; + + const inputChangeHandler = async (event: React.ChangeEvent) => { + const { target: { name, value } } = event; + await authStore.onCredentialsChange(name, value); + }; + + const resendCode = () => { + resendEmailConfirmationCode(); + }; + + const formClassName = classnames( + 'form form--confirm-email', + { 'form--error': authStore.error }, + ); + + return ( +
+
+
+ {translator.getMessage('confirm_email_title')} +
+
+ { + reactTranslator.getMessage('confirm_email_info', { + email: authStore.credentials.username, + span: (chunks: string) => ( + + {chunks} + + ), + }) + } +
+ + {authStore.error && ( +
+ {ReactHtmlParser(authStore.error)} +
+ )} +
+ +
+ +
+ +
+ +
+
+ ); +}); diff --git a/src/popup/components/Authentication/ConfirmEmail/index.ts b/src/popup/components/Authentication/ConfirmEmail/index.ts new file mode 100644 index 000000000..91ffc7dc7 --- /dev/null +++ b/src/popup/components/Authentication/ConfirmEmail/index.ts @@ -0,0 +1 @@ +export { ConfirmEmail } from './ConfirmEmail'; diff --git a/src/popup/components/Authentication/EmailAuth/EmailAuth.tsx b/src/popup/components/Authentication/EmailAuth/EmailAuth.tsx index 966637384..330d66659 100644 --- a/src/popup/components/Authentication/EmailAuth/EmailAuth.tsx +++ b/src/popup/components/Authentication/EmailAuth/EmailAuth.tsx @@ -5,7 +5,7 @@ import ReactHtmlParser from 'react-html-parser'; import classnames from 'classnames'; import { rootStore } from '../../../stores'; -import { RequestStatus } from '../../../stores/consts'; +import { RequestStatus } from '../../../stores/constants'; import { Submit } from '../Submit'; import { InputField } from '../InputField'; import { translator } from '../../../../common/translator'; @@ -45,22 +45,20 @@ export const EmailAuth = observer(() => { onSubmit={submitHandler} >
-
- - {authStore.error && ( -
- {ReactHtmlParser(authStore.error)} -
- )} -
+ + {authStore.error && ( +
+ {ReactHtmlParser(authStore.error)} +
+ )}
{getSubmitButton()} diff --git a/src/popup/components/Authentication/InputField.tsx b/src/popup/components/Authentication/InputField.tsx index b234bfd83..b12cb1772 100644 --- a/src/popup/components/Authentication/InputField.tsx +++ b/src/popup/components/Authentication/InputField.tsx @@ -12,6 +12,8 @@ interface InputFieldParameters { placeholder?: string; label?: string; disabled?: boolean; + title?: string; + autocomplete?: string; } export const InputField = ({ @@ -24,6 +26,8 @@ export const InputField = ({ placeholder = '', label = '', disabled = false, + title = '', + autocomplete, }: InputFieldParameters) => { const inputClassName = classnames( `form__input ${className}`, @@ -43,10 +47,12 @@ export const InputField = ({ type={type} onChange={inputChangeHandler} defaultValue={value} + title={title} placeholder={placeholder} // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus disabled={disabled} + autoComplete={autocomplete} />
diff --git a/src/popup/components/Authentication/RegistrationForm/RegistrationForm.tsx b/src/popup/components/Authentication/RegistrationForm/RegistrationForm.tsx index 3a47b3f7d..549d55e0c 100644 --- a/src/popup/components/Authentication/RegistrationForm/RegistrationForm.tsx +++ b/src/popup/components/Authentication/RegistrationForm/RegistrationForm.tsx @@ -5,7 +5,7 @@ import ReactHtmlParser from 'react-html-parser'; import classnames from 'classnames'; import { rootStore } from '../../../stores'; -import { RequestStatus, InputType } from '../../../stores/consts'; +import { RequestStatus, InputType } from '../../../stores/constants'; import PasswordField from '../PasswordField'; import { Submit } from '../Submit'; import { reactTranslator } from '../../../../common/reactTranslator'; @@ -71,6 +71,7 @@ export const RegistrationForm = observer(() => { id="username" type="email" value={authStore.credentials.username} + title={authStore.credentials.username} label={translator.getMessage('auth_sign_in_provider_adguard_label')} className="form__input__email-disabled" disabled diff --git a/src/popup/components/Authentication/SignInForm/SignInForm.tsx b/src/popup/components/Authentication/SignInForm/SignInForm.tsx index cffb5602f..cefb4be5c 100644 --- a/src/popup/components/Authentication/SignInForm/SignInForm.tsx +++ b/src/popup/components/Authentication/SignInForm/SignInForm.tsx @@ -6,7 +6,7 @@ import classnames from 'classnames'; import { popupActions } from '../../../actions/popupActions'; import { rootStore } from '../../../stores'; -import { RequestStatus, InputType } from '../../../stores/consts'; +import { RequestStatus, InputType } from '../../../stores/constants'; import PasswordField from '../PasswordField'; import { Submit } from '../Submit'; import { reactTranslator } from '../../../../common/reactTranslator'; @@ -56,7 +56,10 @@ export const SignInForm = observer(() => { reactTranslator.getMessage('auth_header_sing_in_notice', { username: authStore.credentials.username, span: (chunks: string) => ( - + {chunks} ), diff --git a/src/popup/components/Authentication/TwoFactorForm/TwoFactorForm.tsx b/src/popup/components/Authentication/TwoFactorForm/TwoFactorForm.tsx index 4eed488e7..c22f7b5dc 100644 --- a/src/popup/components/Authentication/TwoFactorForm/TwoFactorForm.tsx +++ b/src/popup/components/Authentication/TwoFactorForm/TwoFactorForm.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react'; import ReactHtmlParser from 'react-html-parser'; import { rootStore } from '../../../stores'; -import { RequestStatus } from '../../../stores/consts'; +import { RequestStatus } from '../../../stores/constants'; import { Submit } from '../Submit'; import { InputField } from '../InputField'; import { translator } from '../../../../common/translator'; diff --git a/src/popup/components/Authentication/auth.pcss b/src/popup/components/Authentication/auth.pcss index 4647b46ef..88480548a 100644 --- a/src/popup/components/Authentication/auth.pcss +++ b/src/popup/components/Authentication/auth.pcss @@ -9,9 +9,9 @@ &__container { display: flex; flex-direction: column; - width: 88%; + width: 100%; height: 100%; - padding: 0 0 40px; + padding: 0 16px 40px; user-select: none; font-size: 14px; } diff --git a/src/popup/components/ConfirmEmail/ConfirmEmailModal.tsx b/src/popup/components/ConfirmEmail/ConfirmEmailModal.tsx deleted file mode 100644 index 6d5cb1651..000000000 --- a/src/popup/components/ConfirmEmail/ConfirmEmailModal.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { useContext } from 'react'; -import Modal from 'react-modal'; -import { observer } from 'mobx-react'; - -import { rootStore } from '../../stores'; -import { reactTranslator } from '../../../common/reactTranslator'; - -import './confirm-email.pcss'; - -export const ConfirmEmailModal = observer(() => { - const { authStore } = useContext(rootStore); - const { - showConfirmEmailModal, - setShowConfirmEmailModal, - userEmail, - resendConfirmRegistrationLink, - } = authStore; - - const closeModal = () => { - setShowConfirmEmailModal(false); - }; - - const resendLink = () => { - resendConfirmRegistrationLink(); - closeModal(); - }; - - return ( - - - confirm email -
- {reactTranslator.getMessage('confirm_email_title')} -
-
- {reactTranslator.getMessage('confirm_email_info')} -
-
- {userEmail} -
- - -
- ); -}); diff --git a/src/popup/components/ConfirmEmail/ConfirmEmailNotice.tsx b/src/popup/components/ConfirmEmail/ConfirmEmailNotice.tsx deleted file mode 100644 index 1ce022dac..000000000 --- a/src/popup/components/ConfirmEmail/ConfirmEmailNotice.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { useContext } from 'react'; -import { observer } from 'mobx-react'; - -import { rootStore } from '../../stores'; -import { reactTranslator } from '../../../common/reactTranslator'; - -import './confirm-email.pcss'; - -export const ConfirmEmailNotice = observer(() => { - const { authStore } = useContext(rootStore); - const { showConfirmEmailNotice, setShowConfirmEmailModal } = authStore; - - const showConfirmEmailModal = () => { - setShowConfirmEmailModal(true); - }; - - if (!showConfirmEmailNotice) { - return null; - } - - return ( - - ); -}); diff --git a/src/popup/components/ConfirmEmail/confirm-email.pcss b/src/popup/components/ConfirmEmail/confirm-email.pcss deleted file mode 100644 index eb7411234..000000000 --- a/src/popup/components/ConfirmEmail/confirm-email.pcss +++ /dev/null @@ -1,46 +0,0 @@ -.confirm-email { - height: 470px; - top: 55px; - padding-top: 30px; - font-size: 16px; - - &__image { - width: 120px; - margin-bottom: 8px; - } - - &__info { - padding: 0 16px; - line-height: 24px; - } - - &__email { - font-weight: bold; - margin-bottom: 30px; - } - - &__button { - margin-bottom: 18px; - } - - &__notice { - width: 161px; - height: 32px; - border-radius: 4px; - background-color: var(--dark13); - font-size: 14px; - color: var(--white); - position: absolute; - top: 49px; - left: 16px; - display: flex; - align-items: center; - z-index: var(--header-z-index); - border: none; - cursor: pointer; - - & .icon { - margin-right: 3px; - } - } -} diff --git a/src/popup/components/ConfirmEmail/index.ts b/src/popup/components/ConfirmEmail/index.ts deleted file mode 100644 index f02a852e8..000000000 --- a/src/popup/components/ConfirmEmail/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ConfirmEmailModal } from './ConfirmEmailModal'; -export { ConfirmEmailNotice } from './ConfirmEmailNotice'; diff --git a/src/popup/stores/AuthStore.ts b/src/popup/stores/AuthStore.ts index a5f62d949..9f27f5e07 100644 --- a/src/popup/stores/AuthStore.ts +++ b/src/popup/stores/AuthStore.ts @@ -10,8 +10,14 @@ import isNil from 'lodash/isNil'; import { messenger } from '../../lib/messenger'; import { SETTINGS_IDS, FLAGS_FIELDS, SocialAuthProvider } from '../../lib/constants'; import { translator } from '../../common/translator'; +import { + BAD_CREDENTIALS_CODE, + REQUIRED_2FA_CODE, + REQUIRED_EMAIL_CONFIRMATION_CODE, + RESEND_EMAIL_CONFIRMATION_CODE_DELAY_SEC, +} from '../../common/constants'; -import { MAX_GET_POPUP_DATA_ATTEMPTS, RequestStatus } from './consts'; +import { MAX_GET_POPUP_DATA_ATTEMPTS, RequestStatus } from './constants'; import type { RootStore } from './RootStore'; const AUTH_STEPS = { @@ -22,6 +28,7 @@ const AUTH_STEPS = { SIGN_IN: 'signIn', REGISTRATION: 'registration', TWO_FACTOR: 'twoFactor', + CONFIRM_EMAIL: 'confirmEmail', }; enum CredentialsKey { @@ -30,6 +37,7 @@ enum CredentialsKey { ConfirmPassword = 'confirmPassword', TwoFactor = 'twoFactor', MarketingConsent = 'marketingConsent', + Code = 'code', } interface CredentialsInterface { @@ -38,6 +46,7 @@ interface CredentialsInterface { [CredentialsKey.ConfirmPassword]: string; [CredentialsKey.TwoFactor]: string; [CredentialsKey.MarketingConsent]: boolean | string; + [CredentialsKey.Code]: string; } const DEFAULTS = { @@ -47,18 +56,21 @@ const DEFAULTS = { [CredentialsKey.ConfirmPassword]: '', [CredentialsKey.TwoFactor]: '', [CredentialsKey.MarketingConsent]: '', + [CredentialsKey.Code]: '', }, authenticated: false, need2fa: false, error: null, field: '', step: AUTH_STEPS.AUTHORIZATION, + prevSteps: [], agreement: true, policyAgreement: false, helpUsImprove: false, signInCheck: false, showOnboarding: true, showUpgradeScreen: true, + resendCodeCountdown: RESEND_EMAIL_CONFIRMATION_CODE_DELAY_SEC, }; export class AuthStore { @@ -74,6 +86,11 @@ export class AuthStore { @observable step = DEFAULTS.step; + /** + * Needed for BackButton to navigate properly. + */ + @observable prevSteps: string[] = DEFAULTS.prevSteps; + @observable policyAgreement = DEFAULTS.policyAgreement; @observable helpUsImprove = DEFAULTS.helpUsImprove; @@ -98,9 +115,9 @@ export class AuthStore { @observable rating = 0; - @observable showConfirmEmailModal = false; + @observable confirmEmailTimer?: ReturnType; - @observable showConfirmEmailNotice = false; + @observable resendCodeCountdown = DEFAULTS.resendCodeCountdown; @observable userEmail = ''; @@ -150,6 +167,7 @@ export class AuthStore { helpUsImprove, marketingConsent, authError, + code, } = await messenger.getAuthCache(); runInAction(() => { this.credentials = { @@ -158,6 +176,7 @@ export class AuthStore { password, confirmPassword, marketingConsent, + code, }; if (step) { this.step = step; @@ -246,6 +265,16 @@ export class AuthStore { const response = await messenger.authenticateUser(toJS(this.credentials)); if (response.error) { + // user may enter a wrong password but during email confirmation + // the password is to be checked after the valid code is entered. + // so 'bad_credentials' may appear on email confirmation step + // and it should be handled properly + if (response.status === BAD_CREDENTIALS_CODE + && this.step === this.STEPS.CONFIRM_EMAIL) { + // redirect user to password step + this.switchStep(this.STEPS.SIGN_IN, false); + } + runInAction(() => { this.requestProcessState = RequestStatus.Error; }); @@ -266,13 +295,34 @@ export class AuthStore { return; } - if (response.status === '2fa_required') { + if (response.status === REQUIRED_2FA_CODE) { runInAction(async () => { this.requestProcessState = RequestStatus.Done; this.need2fa = true; + this.prevSteps.push(this.step); await this.switchStep(this.STEPS.TWO_FACTOR); }); } + + // handle email confirmation requirement + if (response.status === REQUIRED_EMAIL_CONFIRMATION_CODE) { + if (response.error && !response.authId) { + runInAction(() => { + this.requestProcessState = RequestStatus.Error; + }); + await this.setError(response.error); + return; + } + + this.getResendCodeCountdownAndStart(); + await messenger.setEmailConfirmationAuthId(response.authId); + + runInAction(async () => { + this.requestProcessState = RequestStatus.Done; + this.prevSteps.push(this.step); + await this.switchStep(this.STEPS.CONFIRM_EMAIL); + }); + } }; @action checkEmail = async () => { @@ -288,6 +338,9 @@ export class AuthStore { return; } + // before switching step save the current one to be able to return to it back + this.prevSteps.push(this.step); + if (response.canRegister) { await this.switchStep(this.STEPS.REGISTRATION); } else { @@ -304,8 +357,10 @@ export class AuthStore { await this.setError(translator.getMessage('registration_error_confirm_password')); return; } + this.requestProcessState = RequestStatus.Pending; const response = await messenger.registerUser(toJS(this.credentials)); + if (response.error) { runInAction(() => { this.requestProcessState = RequestStatus.Error; @@ -318,6 +373,7 @@ export class AuthStore { await this.setError(response.error); return; } + if (response.status === 'ok') { await messenger.clearAuthCache(); await this.rootStore.globalStore.getPopupData(MAX_GET_POPUP_DATA_ATTEMPTS); @@ -327,6 +383,24 @@ export class AuthStore { this.credentials = DEFAULTS.credentials; }); } + + if (response.status === REQUIRED_EMAIL_CONFIRMATION_CODE) { + if (response.error && !response.authId) { + runInAction(() => { + this.requestProcessState = RequestStatus.Error; + }); + await this.setError(response.error); + return; + } + + this.getResendCodeCountdownAndStart(); + await messenger.setEmailConfirmationAuthId(response.authId); + + runInAction(async () => { + this.requestProcessState = RequestStatus.Done; + await this.switchStep(this.STEPS.CONFIRM_EMAIL); + }); + } }; @action isAuthenticated = async () => { @@ -374,6 +448,7 @@ export class AuthStore { @action showAuthorizationScreen = async () => { await this.switchStep(this.STEPS.AUTHORIZATION); + this.requestProcessState = RequestStatus.Done; }; @action showScreenShotScreen = async () => { @@ -386,9 +461,9 @@ export class AuthStore { }; @action resetPasswords = async () => { - await messenger.updateAuthCache('password', DEFAULTS.credentials.password); - await messenger.updateAuthCache('confirmPassword', DEFAULTS.credentials.confirmPassword); - await messenger.updateAuthCache('twoFactor', DEFAULTS.credentials.twoFactor); + await messenger.updateAuthCache(CredentialsKey.Password, DEFAULTS.credentials.password); + await messenger.updateAuthCache(CredentialsKey.ConfirmPassword, DEFAULTS.credentials.confirmPassword); + await messenger.updateAuthCache(CredentialsKey.TwoFactor, DEFAULTS.credentials.twoFactor); runInAction(() => { this.credentials.password = DEFAULTS.credentials.password; this.credentials.confirmPassword = DEFAULTS.credentials.confirmPassword; @@ -396,6 +471,17 @@ export class AuthStore { }); }; + @action resetCode = async () => { + await messenger.updateAuthCache(CredentialsKey.Code, DEFAULTS.credentials.code); + runInAction(() => { + this.credentials.code = DEFAULTS.credentials.code; + }); + }; + + @action resetRequestProcessionState = () => { + this.requestProcessState = RequestStatus.Done; + }; + @action openSignInCheck = async () => { await messenger.updateAuthCache('signInCheck', true); @@ -494,21 +580,69 @@ export class AuthStore { this.showRateModal = value; }; - @action setShowConfirmEmail = (value: boolean) => { - this.showConfirmEmailModal = value; - this.showConfirmEmailNotice = value; - }; + /** + * Sets the store's value {@link resendCodeCountdown} to the passed value. + * + * @param value Number of seconds to set. + */ + @action setResendCodeCountdown(value: number): void { + this.resendCodeCountdown = value; + } - @action setShowConfirmEmailModal = (value: boolean) => { - this.showConfirmEmailModal = value; - }; + /** + * Gets count down timer value for email confirmation code resend via messenger, + * and sets it to the store's value {@link resendCodeCountdown}. + */ + @action + async getResendCodeCountdown(): Promise { + let countdown; + try { + countdown = await messenger.getResendCodeCountdown(); + } catch (e) { + countdown = 0; + } + this.setResendCodeCountdown(countdown); + } - @action setUserEmail = (value: string) => { - this.userEmail = value; - }; + /** + * Starts count down timer based on store's value {@link resendCodeCountdown}. + */ + @action startCountdown = () => { + this.confirmEmailTimer = setInterval(() => { + runInAction(() => { + if (this.resendCodeCountdown === 0) { + clearInterval(this.confirmEmailTimer); + return; + } + this.resendCodeCountdown -= 1; + }); + }, 1000); + }; + + /** + * Gets count down timer value for email confirmation code resend from the background, + * and starts count down timer based on it. + * + * Needed for the case when the popup is reopened and the timer was running in the background. + */ + @action + async getResendCodeCountdownAndStart(): Promise { + await this.getResendCodeCountdown(); + if (this.resendCodeCountdown > 0 + // if the timer is already running, don't start it again + && !this.confirmEmailTimer) { + this.startCountdown(); + } + } - @action resendConfirmRegistrationLink = async () => { - await messenger.resendConfirmRegistrationLink(true); + /** + * Resets count down timer, starts it again, and sends a message to the background + * to request a new email confirmation code. + */ + @action resendEmailConfirmationCode = async () => { + this.setResendCodeCountdown(DEFAULTS.resendCodeCountdown); + this.startCountdown(); + await messenger.resendEmailConfirmationCode(); }; @computed @@ -518,7 +652,6 @@ export class AuthStore { return this.showHintPopup && !this.showRateModal && !this.showConfirmRateModal - && !this.showConfirmEmailModal && !this.rootStore.settingsStore.showServerErrorPopup && !this.rootStore.settingsStore.isVpnBlocked // host permissions should be granted to show the hint popup; diff --git a/src/popup/stores/GlobalStore.ts b/src/popup/stores/GlobalStore.ts index cfe2602ec..8bfbf6f78 100644 --- a/src/popup/stores/GlobalStore.ts +++ b/src/popup/stores/GlobalStore.ts @@ -9,7 +9,7 @@ import { tabs } from '../../background/tabs'; import { messenger } from '../../lib/messenger'; import type { RootStore } from './RootStore'; -import { MAX_GET_POPUP_DATA_ATTEMPTS, RequestStatus } from './consts'; +import { MAX_GET_POPUP_DATA_ATTEMPTS, RequestStatus } from './constants'; export class GlobalStore { @observable initStatus = RequestStatus.Pending; @@ -60,7 +60,6 @@ export class GlobalStore { flagsStorageData, isVpnEnabledByUrl, shouldShowRateModal, - username, shouldShowHintPopup, showScreenshotFlow, isVpnBlocked, @@ -94,9 +93,7 @@ export class GlobalStore { authStore.setFlagsStorageData(flagsStorageData); authStore.setIsFirstRun(isFirstRun); - authStore.setUserEmail(username); authStore.setShouldShowRateModal(shouldShowRateModal); - authStore.setShowConfirmEmail(!!vpnInfo?.emailConfirmationRequired); authStore.setShowHintPopup(shouldShowHintPopup); settingsStore.setCanControlProxy(canControlProxy); settingsStore.setConnectivityState(connectivityState); @@ -126,6 +123,7 @@ export class GlobalStore { async init(): Promise { await this.getPopupData(MAX_GET_POPUP_DATA_ATTEMPTS); await this.getDesktopAppData(); + await this.rootStore.authStore.getResendCodeCountdownAndStart(); } @action diff --git a/src/popup/stores/SettingsStore.ts b/src/popup/stores/SettingsStore.ts index 359ee4c62..2d056ab6d 100644 --- a/src/popup/stores/SettingsStore.ts +++ b/src/popup/stores/SettingsStore.ts @@ -22,7 +22,7 @@ import { Prefs } from '../../common/prefs'; import { getThemeFromLocalStorage } from '../../common/useAppearanceTheme'; import type { RootStore } from './RootStore'; -import { MAX_GET_POPUP_DATA_ATTEMPTS, RequestStatus } from './consts'; +import { MAX_GET_POPUP_DATA_ATTEMPTS, RequestStatus } from './constants'; type StateType = { value: string, diff --git a/src/popup/stores/consts.ts b/src/popup/stores/constants.ts similarity index 100% rename from src/popup/stores/consts.ts rename to src/popup/stores/constants.ts diff --git a/src/popup/styles/button.pcss b/src/popup/styles/button.pcss index 548be5424..b3909afdc 100644 --- a/src/popup/styles/button.pcss +++ b/src/popup/styles/button.pcss @@ -184,5 +184,10 @@ button, color: var(--green700); font-size: 14px; text-decoration: none; + + &:disabled { + background: none; + color: var(--grayb8); + } } } diff --git a/src/popup/styles/form.pcss b/src/popup/styles/form.pcss index 467a04cda..4b160e144 100644 --- a/src/popup/styles/form.pcss +++ b/src/popup/styles/form.pcss @@ -3,6 +3,11 @@ flex-direction: column; text-align: center; + &--login, + &--confirm-email { + padding-top: 16px; + } + &--error { & .form__input { border-color: var(--red); @@ -27,9 +32,9 @@ display: block; font-size: 14px; text-align: left; - line-height: 16px; + line-height: 18px; color: var(--gray88); - margin-bottom: 10px; + margin-bottom: 8px; } &__item { @@ -45,7 +50,7 @@ &__inputs { position: relative; - margin: 15px 0 0; + margin: 24px 0 0; text-align: center; } @@ -62,6 +67,8 @@ box-shadow: none; background-color: var(--grayf3); caret-color: var(--gray-base); + overflow: hidden; + text-overflow: ellipsis; &--password { padding: 16px 42px 16px 16px; @@ -125,9 +132,8 @@ } &__btn-wrap { - height: 50px; text-align: center; - margin-bottom: 30px; + margin-bottom: 24px; &--register { margin-bottom: 65px; @@ -166,14 +172,13 @@ } &__link { - min-height: 19px; - margin-bottom: 32px; padding: 0; - font-size: 1.4rem; + font-size: 14px; + line-height: 18px; text-decoration: none; border: 0; background-color: transparent; - color: var(--green76); + color: var(--green700); cursor: pointer; &:hover, @@ -182,7 +187,6 @@ } &--recover { - margin-bottom: 52px; padding: 0 8px; } } @@ -200,18 +204,24 @@ } &__subtitle { - font-size: 2.2rem; + font-size: 24px; font-weight: 700; + line-height: 30px; margin: 0 0 15px; } &__info { - min-height: 44px; - margin-bottom: 10px; + font-size: 16px; + line-height: 24px; + margin-bottom: 24px; } &__credentials { + display: block; font-weight: 500; + margin: 0; + overflow: hidden; + text-overflow: ellipsis; } &__show-password { diff --git a/tests/background/actions.test.ts b/tests/background/actions.test.ts index 325437d40..6149b6cee 100644 --- a/tests/background/actions.test.ts +++ b/tests/background/actions.test.ts @@ -16,6 +16,7 @@ jest.mock('../../src/background/config', () => { UPGRADE_LICENSE_URL: 'https://adguard-vpn.com/license.html?action=upgrade_license', }; }); +jest.mock('../../src/background/settings'); describe('Actions tests', () => { beforeEach(async () => { diff --git a/tests/background/exclusions/ExclusionsTree.test.ts b/tests/background/exclusions/ExclusionsTree.test.ts index 0bd92c66f..915cb4665 100644 --- a/tests/background/exclusions/ExclusionsTree.test.ts +++ b/tests/background/exclusions/ExclusionsTree.test.ts @@ -6,6 +6,7 @@ import type { ExclusionInterface } from '../../../src/background/schema'; import { ServicesInterface } from '../../../src/background/schema'; jest.mock('../../../src/lib/logger.ts'); +jest.mock('../../../src/background/providers/vpnProvider.ts'); jest.mock('../../../src/background/browserApi', () => { return { diff --git a/tests/background/providers/authProvider.test.ts b/tests/background/providers/authProvider.test.ts index 53f1de266..59eccfe50 100644 --- a/tests/background/providers/authProvider.test.ts +++ b/tests/background/providers/authProvider.test.ts @@ -80,7 +80,10 @@ describe('authProvider', () => { error_description: 'Sorry, unrecognized username or password', }, false); - const expectedError = new Error(JSON.stringify({ error: 'authentication_error_wrong_credentials' })); + const expectedError = new Error(JSON.stringify({ + error: 'authentication_error_wrong_credentials', + status: 'bad_credentials', + })); await expect(authProvider.getAccessToken(emptyCredentials)).rejects.toThrow(expectedError); }); diff --git a/tests/background/settings/SettingsService.test.ts b/tests/background/settings/SettingsService.test.ts index a63dccb65..3b67df02b 100644 --- a/tests/background/settings/SettingsService.test.ts +++ b/tests/background/settings/SettingsService.test.ts @@ -16,6 +16,8 @@ jest.mock('../../../src/background/browserApi', () => { }; }); +jest.mock('../../../src/background/settings'); + const storage = (() => { const settingsStorage: { [key: string]: any } = {}; return { diff --git a/yarn.lock b/yarn.lock index bbfb87b3a..0d6e8a85b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3892,9 +3892,9 @@ camelcase@^6.0.0, camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001489, caniuse-lite@^1.0.30001503: - version "1.0.30001517" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz#90fabae294215c3495807eb24fc809e11dc2f0a8" - integrity sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA== + version "1.0.30001579" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz" + integrity sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA== capture-exit@^2.0.0: version "2.0.0"