Skip to content

Commit

Permalink
Farzin/POC/74924/Add useStore and useWS hooks (#6339)
Browse files Browse the repository at this point in the history
* fix(cashier): 🐛 fix `tsconfig`

* feat(shared): ✨ expose `WS` object from `shared` package via `useWS`

* feat(cashier): ✨ add `useCountdown` hook

* feat(cashier): ✨ add `useWS` hook

* feat(cashier): ✨ add `useStore` hook and `StoreContext`

* feat(cashier): ✨ add `useVerifyEmail` hook

* test(cashier): ✅ add test for `useCountdown` hook

* test(cashier): ✅ add test for `useWS` hook

* fix(cashier): 📝 resolve PR comments

* fix(cashier): 📝 resolve PR comments

* fix(cashier): 📝 resolve PR comments

* fix(cashier): 📝 resolve PR comments

* refactor(cashier): ♻️ improve types for `useWS` hook
  • Loading branch information
farzin-deriv committed Oct 6, 2022
1 parent d5e51ff commit f982ade
Show file tree
Hide file tree
Showing 15 changed files with 490 additions and 9 deletions.
1 change: 1 addition & 0 deletions packages/cashier/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@testing-library/react": "^12.0.0",
"@types/react": "^18.0.7",
"@types/react-dom": "^18.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/qrcode.react": "^1.0.2",
"babel-loader": "^8.1.0",
"clean-webpack-plugin": "^3.0.0",
Expand Down
5 changes: 4 additions & 1 deletion packages/cashier/src/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { setWebsocket } from '@deriv/shared';
import { init } from 'Utils/server_time';
import Routes from 'Containers/routes';
import { MobxContentProvider } from 'Stores/connect';
import { StoreProvider } from './hooks';

const App = ({ passthrough: { WS, root_store } }) => {
React.useEffect(() => {
Expand All @@ -13,7 +14,9 @@ const App = ({ passthrough: { WS, root_store } }) => {

return (
<MobxContentProvider store={root_store}>
<Routes />
<StoreProvider store={root_store}>
<Routes />
</StoreProvider>
</MobxContentProvider>
);
};
Expand Down
157 changes: 157 additions & 0 deletions packages/cashier/src/hooks/__tests__/useCountdown.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import * as React from 'react';
// Todo: After upgrading to react 18 we should use @testing-library/react-hooks instead.
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import useCountdown, { TCountdownOptions } from '../useCountdown';

jest.setTimeout(10000);

const UseCountdownExample = (props: TCountdownOptions) => {
const counter = useCountdown(props);

return (
<React.Fragment>
<p data-testid={'dt_count'}>{counter.count}</p>
<p data-testid={'dt_is_running'}>{counter.is_running ? 'true' : 'false'}</p>
<button data-testid={'dt_start'} onClick={counter.start}>
start
</button>
<button data-testid={'dt_pause'} onClick={counter.pause}>
pause
</button>
<button data-testid={'dt_stop'} onClick={counter.stop}>
stop
</button>
<button data-testid={'dt_reset'} onClick={counter.reset}>
reset
</button>
</React.Fragment>
);
};

describe('useCountdown', () => {
test('should have initial count of 10 and is_running of false', () => {
render(<UseCountdownExample from={10} />);

const count = screen.getByTestId('dt_count');
const is_running = screen.getByTestId('dt_is_running');

expect(count).toHaveTextContent('10');
expect(is_running).toHaveTextContent('false');
});

test('should count down from 5 to 0 after start is called and stop once finished', async () => {
render(<UseCountdownExample from={5} />);

const count = screen.getByTestId('dt_count');
const is_running = screen.getByTestId('dt_is_running');
const start = screen.getByTestId('dt_start');

expect(count).toHaveTextContent('5');
expect(is_running).toHaveTextContent('false');
userEvent.click(start);
await waitFor(() => expect(is_running).toHaveTextContent('true'));
await waitFor(() => expect(count).toHaveTextContent('4'));
await waitFor(() => expect(count).toHaveTextContent('3'));
await waitFor(() => expect(count).toHaveTextContent('2'));
await waitFor(() => expect(count).toHaveTextContent('1'));
await waitFor(() => expect(count).toHaveTextContent('0'));
await waitFor(() => expect(is_running).toHaveTextContent('false'));
});

test('should count down from 2 to -2 after start is called and stop once finished', async () => {
render(<UseCountdownExample from={2} to={-2} />);

const count = screen.getByTestId('dt_count');
const is_running = screen.getByTestId('dt_is_running');
const start = screen.getByTestId('dt_start');

expect(count).toHaveTextContent('2');
expect(is_running).toHaveTextContent('false');
userEvent.click(start);
await waitFor(() => expect(is_running).toHaveTextContent('true'));
await waitFor(() => expect(count).toHaveTextContent('1'));
await waitFor(() => expect(count).toHaveTextContent('0'));
await waitFor(() => expect(count).toHaveTextContent('-1'));
await waitFor(() => expect(count).toHaveTextContent('-2'));
await waitFor(() => expect(is_running).toHaveTextContent('false'));
});

test('should count down from -2 to 2 after start is called and stop once finished', async () => {
render(<UseCountdownExample from={-2} to={2} increment />);

const count = screen.getByTestId('dt_count');
const is_running = screen.getByTestId('dt_is_running');
const start = screen.getByTestId('dt_start');

expect(count).toHaveTextContent('-2');
expect(is_running).toHaveTextContent('false');
userEvent.click(start);
await waitFor(() => expect(is_running).toHaveTextContent('true'));
await waitFor(() => expect(count).toHaveTextContent('-1'));
await waitFor(() => expect(count).toHaveTextContent('0'));
await waitFor(() => expect(count).toHaveTextContent('1'));
await waitFor(() => expect(count).toHaveTextContent('2'));
await waitFor(() => expect(is_running).toHaveTextContent('false'));
});

test('should count down from 3 to 0 after start is called and reset the counter at 1 and stop once finished', async () => {
render(<UseCountdownExample from={3} />);

const count = screen.getByTestId('dt_count');
const is_running = screen.getByTestId('dt_is_running');
const start = screen.getByTestId('dt_start');
const reset = screen.getByTestId('dt_reset');

expect(count).toHaveTextContent('3');
expect(is_running).toHaveTextContent('false');
userEvent.click(start);
await waitFor(() => expect(is_running).toHaveTextContent('true'));
await waitFor(() => expect(count).toHaveTextContent('2'));
await waitFor(() => expect(count).toHaveTextContent('1'));
userEvent.click(reset);
await waitFor(() => expect(count).toHaveTextContent('3'));
await waitFor(() => expect(count).toHaveTextContent('2'));
await waitFor(() => expect(count).toHaveTextContent('1'));
await waitFor(() => expect(count).toHaveTextContent('0'));
await waitFor(() => expect(is_running).toHaveTextContent('false'));
});

test('should count down from 3 to 0 after start is called and pause the counter at 1', async () => {
render(<UseCountdownExample from={3} />);

const count = screen.getByTestId('dt_count');
const is_running = screen.getByTestId('dt_is_running');
const start = screen.getByTestId('dt_start');
const pause = screen.getByTestId('dt_pause');

expect(count).toHaveTextContent('3');
expect(is_running).toHaveTextContent('false');
userEvent.click(start);
await waitFor(() => expect(is_running).toHaveTextContent('true'));
await waitFor(() => expect(count).toHaveTextContent('2'));
await waitFor(() => expect(count).toHaveTextContent('1'));
userEvent.click(pause);
await waitFor(() => expect(count).toHaveTextContent('1'));
await waitFor(() => expect(is_running).toHaveTextContent('false'));
});

test('should count down from 3 to 0 after start is called and stop the counter at 1', async () => {
render(<UseCountdownExample from={3} />);

const count = screen.getByTestId('dt_count');
const is_running = screen.getByTestId('dt_is_running');
const start = screen.getByTestId('dt_start');
const stop = screen.getByTestId('dt_stop');

expect(count).toHaveTextContent('3');
expect(is_running).toHaveTextContent('false');
userEvent.click(start);
await waitFor(() => expect(is_running).toHaveTextContent('true'));
await waitFor(() => expect(count).toHaveTextContent('2'));
await waitFor(() => expect(count).toHaveTextContent('1'));
userEvent.click(stop);
await waitFor(() => expect(count).toHaveTextContent('3'));
await waitFor(() => expect(is_running).toHaveTextContent('false'));
});
});
94 changes: 94 additions & 0 deletions packages/cashier/src/hooks/__tests__/useWS.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as React from 'react';
// Todo: After upgrading to react 18 we should use @testing-library/react-hooks instead.
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useWS as useWSShared } from '@deriv/shared';
import useWS from '../useWS';
import { TSocketEndpointNames, TSocketRequestProps } from '../../types';

jest.mock('@deriv/shared');

const mockUseWSShared = useWSShared as jest.MockedFunction<typeof useWSShared>;

const UseWSExample = <T extends TSocketEndpointNames>({
name,
request,
}: {
name: T;
request?: TSocketRequestProps<T>;
}) => {
const WS = useWS(name);

return (
<React.Fragment>
<p data-testid={'dt_is_loading'}>{WS.is_loading ? 'true' : 'false'}</p>
<p data-testid={'dt_error'}>{WS.error ? JSON.stringify(WS.error) : 'undefined'}</p>
<p data-testid={'dt_data'}>{WS.data ? JSON.stringify(WS.data) : 'undefined'}</p>
<button data-testid={'dt_send'} onClick={() => WS.send(request)}>
send
</button>
</React.Fragment>
);
};

describe('useWS', () => {
test('should have initial error and data of undefined and is_loading of false', async () => {
render(<UseWSExample name={'ping'} />);

const is_loading = screen.getByTestId('dt_is_loading');
const error = screen.getByTestId('dt_error');
const data = screen.getByTestId('dt_data');

expect(is_loading).toHaveTextContent('false');
expect(error).toHaveTextContent('undefined');
expect(data).toHaveTextContent('undefined');
});

test('should call ping and get pong in response', async () => {
mockUseWSShared.mockReturnValue({
send: jest.fn(() => Promise.resolve({ ping: 'pong' })),
});

render(<UseWSExample name={'ping'} />);

const is_loading = screen.getByTestId('dt_is_loading');
const error = screen.getByTestId('dt_error');
const data = screen.getByTestId('dt_data');
const send = screen.getByTestId('dt_send');

expect(is_loading).toHaveTextContent('false');
expect(error).toHaveTextContent('undefined');
expect(data).toHaveTextContent('undefined');
userEvent.click(send);
await waitFor(() => expect(is_loading).toHaveTextContent('true'));
await waitFor(() => expect(data).toHaveTextContent('pong'));
await waitFor(() => expect(error).toHaveTextContent('undefined'));
await waitFor(() => expect(is_loading).toHaveTextContent('false'));
});

test('should call verify_email and get 1 in response', async () => {
mockUseWSShared.mockReturnValue({
send: jest.fn(() => Promise.resolve({ verify_email: 1 })),
});

render(
<UseWSExample name={'verify_email'} request={{ verify_email: 'test@test.com', type: 'reset_password' }} />
);

const is_loading = screen.getByTestId('dt_is_loading');
const error = screen.getByTestId('dt_error');
const data = screen.getByTestId('dt_data');
const send = screen.getByTestId('dt_send');

expect(is_loading).toHaveTextContent('false');
expect(error).toHaveTextContent('undefined');
expect(data).toHaveTextContent('undefined');
userEvent.click(send);
await waitFor(() => expect(is_loading).toHaveTextContent('true'));
await waitFor(() => expect(data).toHaveTextContent('1'));
await waitFor(() => expect(error).toHaveTextContent('undefined'));
await waitFor(() => expect(is_loading).toHaveTextContent('false'));
});

// TODO: Add more test cases.
});
4 changes: 4 additions & 0 deletions packages/cashier/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './useStore';
export { default as useCountdown } from './useCountdown';
export { default as useWS } from './useWS';
export { default as useVerifyEmail } from './useVerifyEmail';
52 changes: 52 additions & 0 deletions packages/cashier/src/hooks/useCountdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useEffect, useState } from 'react';

const ONE_SECOND = 1000;

export type TCountdownOptions = {
from: number;
to?: number;
increment?: boolean;
};

const useCountdown = ({ from, to = 0, increment = false }: TCountdownOptions) => {
const [count, setCount] = useState(from);
const [is_running, setIsRunning] = useState(false);

useEffect(() => {
let timer: NodeJS.Timeout;

if (is_running) {
timer = setTimeout(() => {
if (count === to) {
pause();
} else {
setCount(old => (increment ? old + 1 : old - 1));
}
}, ONE_SECOND);
}

return () => clearTimeout(timer);
}, [count, is_running, to, increment]);

const start = () => setIsRunning(true);

const pause = () => setIsRunning(false);

const reset = () => setCount(from);

const stop = () => {
pause();
reset();
};

return {
count,
is_running,
start,
pause,
reset,
stop,
};
};

export default useCountdown;
18 changes: 18 additions & 0 deletions packages/cashier/src/hooks/useStore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React, { createContext, PropsWithChildren, useContext } from 'react';
import { TRootStore } from 'Types';

export const StoreContext = createContext<TRootStore | null>(null);

export const StoreProvider = ({ children, store }: PropsWithChildren<{ store: TRootStore }>) => {
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
};

export const useStore = () => {
const store = useContext(StoreContext);

if (!store) {
throw new Error('useStore must be used within StoreContext');
}

return store;
};
41 changes: 41 additions & 0 deletions packages/cashier/src/hooks/useVerifyEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useState } from 'react';
import { TSocketEndpoints } from 'Types';
import useCountdown from './useCountdown';
import { useStore } from './useStore';
import useWS from './useWS';

const RESEND_COUNTDOWN = 60;

export type TEmailVerificationType = TSocketEndpoints['verify_email']['request']['type'];

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 send = () => {
if (!client.email) return;
if (counter.is_running) return;

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

setSentCount(old => old + 1);

WS.send({ verify_email: client.email, type });
};

return {
is_loading: WS.is_loading,
error: WS.error,
data: WS.data,
counter: counter.count,
is_counter_running: counter.is_running,
sent_count,
has_been_sent: sent_count !== 0,
send,
};
};

export default useVerifyEmail;
Loading

1 comment on commit f982ade

@vercel
Copy link

@vercel vercel bot commented on f982ade Oct 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

deriv-app – ./

binary.sx
deriv-app.binary.sx
deriv-app-git-master.binary.sx
deriv-app.vercel.app

Please sign in to comment.