Skip to content

Commit

Permalink
Maryia/80948/fix: avoid rate limit error by storing counter after sen…
Browse files Browse the repository at this point in the history
…ding verification email in Withdrawal (deriv-com#7218)

* fix: instantiating useVerifyEmail object once for each email verification case to avoid settimeout leak in useCountdown

* chore: store verify_email_sent_count in store instead of locally

* test: fix test for email-verification-empty-state.tsx

* fix: avoid counter reset by storing and using the time an email was last sent

* chore: a workaround for VerifyEmailRequest type declared with an extra required field in api-types

* chore: remove api-types workaround

* build: update @deriv/api-types to 1.0.85

* revert: api-types version upgrade

* revert: package-lock

* revert: modal-manager.jsx

* revert: modal-manager.jsx
  • Loading branch information
maryia-deriv authored Mar 21, 2023
1 parent 3ce03d5 commit cc0065d
Show file tree
Hide file tree
Showing 8 changed files with 59 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react';
import { render, screen } from '@testing-library/react';
import EmailVerificationEmptyState from '../email-verification-empty-state';
import { TRootStore } from 'Types';
import { useVerifyEmail } from '@deriv/hooks';
import { VerifyEmail } from '@deriv/api-types';
import CashierProviders from '../../../cashier-providers';

const mock_store: DeepPartial<TRootStore> = {
Expand All @@ -11,8 +13,18 @@ const mock_store: DeepPartial<TRootStore> = {
};

describe('EmailVerificationEmptyState', () => {
const verify: ReturnType<typeof useVerifyEmail> = {
is_loading: false,
error: '',
data: {} as VerifyEmail,
counter: 58,
is_counter_running: true,
sent_count: 2,
has_been_sent: true,
send: jest.fn(),
};
test('should disable resend button after sending the request', () => {
render(<EmailVerificationEmptyState type='reset_password' />, {
render(<EmailVerificationEmptyState verify={verify} />, {
wrapper: ({ children }) => <CashierProviders store={mock_store as TRootStore}>{children}</CashierProviders>,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import React from 'react';
import { useVerifyEmail, TEmailVerificationType } from '@deriv/hooks';
import { useVerifyEmail } from '@deriv/hooks';
import { localize } from '@deriv/translations';
import EmptyState from 'Components/empty-state';
import EmailVerificationResendEmptyState from './email-verification-resend-empty-state';
import './email-verification-empty-state.scss';

type TEmailVerificationEmptyStateProps = {
type: TEmailVerificationType;
verify: ReturnType<typeof useVerifyEmail>;
};

const EmailVerificationEmptyState = ({ type }: TEmailVerificationEmptyStateProps) => {
const verify = useVerifyEmail(type);

const EmailVerificationEmptyState = ({ verify }: TEmailVerificationEmptyStateProps) => {
const action = {
label: localize("Didn't receive the email?"),
onClick: verify.send,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ const WithdrawalTab = observer(() => {
// match the behavior of the `Withdrawal` page and first inform the user.

if (verify.error && 'code' in verify.error) return <PaymentAgentWithdrawalLocked error={verify.error} />;
if (!verify.is_loading && verify.has_been_sent)
return <EmailVerificationEmptyState type={'paymentagent_withdraw'} />;
if (!verify.is_loading && verify.has_been_sent) return <EmailVerificationEmptyState verify={verify} />;
if (verification_code || payment_agent.is_withdraw)
return <PaymentAgentContainer verification_code={verification_code} />;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const WithdrawalVerificationEmail = observer(() => {

if (verify.error) return <Error error={verify.error} />;

if (verify.has_been_sent) return <EmailVerificationEmptyState type={'payment_withdraw'} />;
if (verify.has_been_sent) return <EmailVerificationEmptyState verify={verify} />;

return (
<>
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/Stores/client-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export default class ClientStore extends BaseStore {

account_limits = {};
self_exclusion = {};
sent_verify_emails_data = {};

local_currency_config = {
currency: '',
Expand Down Expand Up @@ -206,6 +207,7 @@ export default class ClientStore extends BaseStore {
new_email: observable,
account_limits: observable,
self_exclusion: observable,
sent_verify_emails_data: observable,
local_currency_config: observable,
has_cookie_account: observable,
financial_assessment: observable,
Expand Down Expand Up @@ -310,6 +312,7 @@ export default class ClientStore extends BaseStore {
setCookieAccount: action.bound,
setCFDScore: action.bound,
setIsCFDScoreAvailable: action.bound,
setSentVerifyEmailsData: action.bound,
updateSelfExclusion: action.bound,
responsePayoutCurrencies: action.bound,
responseAuthorize: action.bound,
Expand Down Expand Up @@ -1180,6 +1183,11 @@ export default class ClientStore extends BaseStore {
LocalStore.setObject(LANGUAGE_KEY, lang);
};

setSentVerifyEmailsData(sent_verify_emails_data) {
this.sent_verify_emails_data = sent_verify_emails_data;
LocalStore.setObject('sent_verify_emails_data', sent_verify_emails_data);
}

setCookieAccount() {
const domain = /deriv\.(com|me)/.test(window.location.hostname) ? deriv_urls.DERIV_HOST_NAME : 'binary.sx';

Expand Down Expand Up @@ -1589,6 +1597,7 @@ export default class ClientStore extends BaseStore {

this.setLoginId(LocalStore.get('active_loginid'));
this.setAccounts(LocalStore.getObject(storage_key));
this.setSentVerifyEmailsData(LocalStore.getObject('sent_verify_emails_data'));
this.setSwitched('');
const client = this.accounts[this.loginid];
// If there is an authorize_response, it means it was the first login
Expand Down
22 changes: 17 additions & 5 deletions packages/hooks/src/useVerifyEmail.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useState } from 'react';
import { useWS } from '@deriv/api';
import { useStore } from '@deriv/stores';
import type { TSocketEndpoints } from '@deriv/api/types';
Expand All @@ -10,18 +9,31 @@ export type TEmailVerificationType = TSocketEndpoints['verify_email']['request']

const useVerifyEmail = (type: TEmailVerificationType) => {
const WS = useWS('verify_email');
const counter = useCountdown({ from: RESEND_COUNTDOWN });
const { client } = useStore();
const [sent_count, setSentCount] = useState(0);
const { setSentVerifyEmailsData, sent_verify_emails_data } = client;
const { last_time_sent_seconds = 0, sent_count = 0 } = sent_verify_emails_data[type] || {};
const time_now_seconds = Math.floor(Date.now() / 1000);
const seconds_left = last_time_sent_seconds + RESEND_COUNTDOWN - time_now_seconds;
const should_not_allow_resend =
last_time_sent_seconds && time_now_seconds < last_time_sent_seconds + RESEND_COUNTDOWN;
const countdown = should_not_allow_resend ? seconds_left : RESEND_COUNTDOWN;
const counter = useCountdown({ from: countdown });

if (!counter.is_running && should_not_allow_resend) {
counter.start();
}

const send = () => {
if (!client.email) return;
if (counter.is_running) return;

counter.reset();
counter.start();

setSentCount(old => old + 1);
const sent_emails_data = {
...sent_verify_emails_data,
[type]: { last_time_sent_seconds: time_now_seconds, sent_count: sent_count + 1 },
};
setSentVerifyEmailsData(sent_emails_data);

WS.send({ verify_email: client.email, type });
};
Expand Down
2 changes: 2 additions & 0 deletions packages/stores/src/mockStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ const mock = (): TRootStore => {
residence: '',
responseMt5LoginList: jest.fn(),
responseTradingPlatformAccountsList: jest.fn(),
sent_verify_emails_data: {},
setSentVerifyEmailsData: jest.fn(),
standpoint: {
iom: '',
},
Expand Down
13 changes: 13 additions & 0 deletions packages/stores/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { GetAccountStatus, Authorize, DetailsOfEachMT5Loginid, LogOutResponse, GetLimits } from '@deriv/api-types';
import type { TEmailVerificationType } from '@deriv/hooks';
import type { RouteComponentProps } from 'react-router';

type TAccount = NonNullable<Authorize['account_list']>[0];
Expand Down Expand Up @@ -167,7 +168,9 @@ type TClientStore = {
trading_platform_dxtrade_password_reset: string;
trading_platform_mt5_password_reset: string;
};
sent_verify_emails_data: TSentVerifyEmailsData;
email: string;
setSentVerifyEmailsData: (sent_verify_emails_data: TSentVerifyEmailsData) => void;
setVerificationCode: (code: string, action: string) => void;
updateAccountStatus: () => Promise<void>;
is_authentication_needed: boolean;
Expand All @@ -179,6 +182,16 @@ type TClientStore = {
is_crypto: boolean;
};

type TSentVerifyEmailsData = Partial<
Record<
TEmailVerificationType,
{
last_time_sent_seconds: number;
sent_count: number;
}
>
>;

type TCommonStoreError = {
header: string | JSX.Element;
message: string | JSX.Element;
Expand Down

0 comments on commit cc0065d

Please sign in to comment.