-
Notifications
You must be signed in to change notification settings - Fork 298
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
d5e51ff
commit f982ade
Showing
15 changed files
with
490 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
157 changes: 157 additions & 0 deletions
157
packages/cashier/src/hooks/__tests__/useCountdown.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.
f982ade
There was a problem hiding this comment.
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