From dcd70131be0e6db9c6645737f1d13be017f24386 Mon Sep 17 00:00:00 2001 From: Bahar Date: Thu, 13 Oct 2022 16:50:21 +0800 Subject: [PATCH 1/2] extract_copy_to_clipboard_to_hook --- .../api-token/__tests__/api-token.spec.js | 11 ---- .../api-token/api-token-clipboard.jsx | 15 +---- .../__tests__/on-ramp-provider-popup.spec.tsx | 17 ----- .../on-ramp-provider-popup.tsx | 33 +++++----- .../stores/__tests__/on-ramp-store.spec.js | 42 ------------- packages/cashier/src/stores/on-ramp-store.js | 29 --------- packages/cashier/src/utils/utility.js | 10 +-- .../src/components/clipboard/clipboard.jsx | 62 ++++++++----------- packages/components/src/hooks/index.js | 1 + .../src/hooks/use-copy-to-clipboard.js | 18 ++++++ packages/core/src/_common/utility.js | 9 --- 11 files changed, 65 insertions(+), 182 deletions(-) create mode 100644 packages/components/src/hooks/use-copy-to-clipboard.js diff --git a/packages/account/src/Components/api-token/__tests__/api-token.spec.js b/packages/account/src/Components/api-token/__tests__/api-token.spec.js index 28ef8828a07a..546d0321334f 100644 --- a/packages/account/src/Components/api-token/__tests__/api-token.spec.js +++ b/packages/account/src/Components/api-token/__tests__/api-token.spec.js @@ -306,7 +306,6 @@ describe('', () => { fireEvent.click(copy_btns_1[0]); expect(screen.queryByText(warning_msg)).not.toBeInTheDocument(); - expect(await screen.findByTestId('dt_token_copied_icon')).toBeInTheDocument(); act(() => jest.advanceTimersByTime(2100)); expect(screen.queryByTestId('dt_token_copied_icon')).not.toBeInTheDocument(); @@ -314,20 +313,10 @@ describe('', () => { fireEvent.click(copy_btns_1[1]); expect(await screen.findByText(warning_msg)).toBeInTheDocument(); - expect(document.execCommand).toHaveBeenCalledTimes(1); - const ok_btn = screen.getByRole('button', { name: /ok/i }); expect(ok_btn).toBeInTheDocument(); fireEvent.click(ok_btn); - expect(await screen.findByTestId('dt_token_copied_icon')).toBeInTheDocument(); - const copy_btns_2 = await screen.findAllByTestId('dt_copy_token_icon'); - expect(copy_btns_2.length).toBe(1); - - act(() => jest.advanceTimersByTime(2100)); - expect(screen.queryByTestId('dt_token_copied_icon')).not.toBeInTheDocument(); - - expect(document.execCommand).toHaveBeenCalledTimes(2); jest.clearAllMocks(); }); diff --git a/packages/account/src/Components/api-token/api-token-clipboard.jsx b/packages/account/src/Components/api-token/api-token-clipboard.jsx index f88871ce12ab..94788c0781b4 100644 --- a/packages/account/src/Components/api-token/api-token-clipboard.jsx +++ b/packages/account/src/Components/api-token/api-token-clipboard.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useIsMounted } from '@deriv/shared'; -import { Button, Icon, Modal, Text, Popover } from '@deriv/components'; +import { Button, Icon, Modal, Text, Popover, useCopyToClipboard } from '@deriv/components'; import { localize } from '@deriv/translations'; const WarningNoteBullet = ({ message }) => ( @@ -29,7 +29,7 @@ const WarningDialogMessage = () => ( ); const ApiTokenClipboard = ({ scopes, text_copy, info_message, success_message, popover_alignment = 'bottom' }) => { - const [is_copied, setIsCopied] = React.useState(false); + const [is_copied, copyToClipboard, setIsCopied] = useCopyToClipboard(); const [is_modal_open, setIsModalOpen] = React.useState(false); const [is_popover_open, setIsPopoverOpen] = React.useState(false); const isMounted = useIsMounted(); @@ -45,15 +45,6 @@ const ApiTokenClipboard = ({ scopes, text_copy, info_message, success_message, p if (!is_copied) setIsPopoverOpen(false); }; - const copyToClipboard = text => { - const textField = document.createElement('textarea'); - textField.innerText = text; - document.body.appendChild(textField); - textField.select(); - document.execCommand('copy'); - textField.remove(); - }; - /* two timeouts help to prevent popup window blinking. without early hiding the popup we will see shortly the description message like during hovering. this bug appears when popup is handled outside like here @@ -61,11 +52,11 @@ const ApiTokenClipboard = ({ scopes, text_copy, info_message, success_message, p const onClick = () => { setIsModalOpen(false); copyToClipboard(text_copy); - setIsCopied(true); setIsPopoverOpen(true); timeout_clipboard = setTimeout(() => { if (isMounted()) { setIsPopoverOpen(false); + setIsCopied(false); } }, 1900); timeout_clipboard_2 = setTimeout(() => { diff --git a/packages/cashier/src/pages/on-ramp/on-ramp-provider-popup/__tests__/on-ramp-provider-popup.spec.tsx b/packages/cashier/src/pages/on-ramp/on-ramp-provider-popup/__tests__/on-ramp-provider-popup.spec.tsx index bea6c0561955..b0dfc0ba9186 100644 --- a/packages/cashier/src/pages/on-ramp/on-ramp-provider-popup/__tests__/on-ramp-provider-popup.spec.tsx +++ b/packages/cashier/src/pages/on-ramp/on-ramp-provider-popup/__tests__/on-ramp-provider-popup.spec.tsx @@ -18,7 +18,6 @@ describe('', () => { api_error: '', deposit_address: 'tb1qhux20f7h42ya9nqdntl6r9p7p264a2ct8t3n6p', is_deposit_address_loading: false, - is_deposit_address_popover_open: false, is_requesting_widget_html: false, selected_provider: { name: 'Changelly', @@ -29,7 +28,6 @@ describe('', () => { should_show_widget: false, widget_error: '', widget_html: 'Widget HTML', - onClickCopyDepositAddress: jest.fn(), onClickDisclaimerContinue: jest.fn(), onClickGoToDepositPage: jest.fn(), setIsOnRampModalOpen: jest.fn(), @@ -112,15 +110,6 @@ describe('', () => { expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument(); }); - it('should trigger onClick callback when the user clicks on copy icon', () => { - render(); - - const copy_icon = screen.getByTestId('dti_deposit_address_icon'); - fireEvent.click(copy_icon); - - expect(props.onClickCopyDepositAddress).toHaveBeenCalledTimes(1); - }); - it('should trigger onFocus method when the user clicks on deposit address field', () => { render(); @@ -128,12 +117,6 @@ describe('', () => { expect(fireEvent.focus(deposit_address_input)).toBeTruthy(); }); - it('should show "Copied!" message', () => { - render(); - - expect(screen.getByText('Copied!')).toBeInTheDocument(); - }); - it('should trigger onClick calbacks when the user clicks on "Cancel" and "Continue" buttons', () => { const { rerender } = render(); diff --git a/packages/cashier/src/pages/on-ramp/on-ramp-provider-popup/on-ramp-provider-popup.tsx b/packages/cashier/src/pages/on-ramp/on-ramp-provider-popup/on-ramp-provider-popup.tsx index 9965b588f4d7..3d27c6e7953d 100644 --- a/packages/cashier/src/pages/on-ramp/on-ramp-provider-popup/on-ramp-provider-popup.tsx +++ b/packages/cashier/src/pages/on-ramp/on-ramp-provider-popup/on-ramp-provider-popup.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import React from 'react'; -import { Button, HintBox, Icon, Loading, Popover, Text } from '@deriv/components'; +import { Button, HintBox, Icon, Loading, Popover, Text, useCopyToClipboard } from '@deriv/components'; import { getKebabCase, website_name, isMobile } from '@deriv/shared'; import { localize, Localize } from '@deriv/translations'; import { connect } from 'Stores/connect'; @@ -11,13 +11,10 @@ type TOnRampProviderPopupProps = { deposit_address: string; is_dark_mode_on: TUiStore['is_dark_mode_on']; is_deposit_address_loading: boolean; - is_deposit_address_popover_open: boolean; is_requesting_widget_html: boolean; - onClickCopyDepositAddress: () => void; onClickDisclaimerContinue: () => void; onClickGoToDepositPage: () => void; selected_provider: TProviderDetails; - setDepositAddressRef: (ref: HTMLDivElement | null) => void; setIsOnRampModalOpen: (boolean: boolean) => void; should_show_dialog: boolean; should_show_widget: boolean; @@ -30,13 +27,10 @@ const OnRampProviderPopup = ({ deposit_address, is_dark_mode_on, is_deposit_address_loading, - is_deposit_address_popover_open, is_requesting_widget_html, - onClickCopyDepositAddress, onClickDisclaimerContinue, onClickGoToDepositPage, selected_provider, - setDepositAddressRef, setIsOnRampModalOpen, should_show_dialog, should_show_widget, @@ -44,6 +38,20 @@ const OnRampProviderPopup = ({ widget_html, }: TOnRampProviderPopupProps) => { const el_onramp_widget_container_ref = React.useRef(null); + const [is_copied, copyToClipboard, setIsCopied] = useCopyToClipboard(); + let timeout_clipboard: ReturnType; + + const onClickCopyDepositAddress = () => { + copyToClipboard(deposit_address); + + timeout_clipboard = setTimeout(() => { + setIsCopied(false); + }, 500); + }; + + React.useEffect(() => { + return () => clearTimeout(timeout_clipboard); + }, []); // JS executed after "on-ramp__widget-container" has been added to the DOM. // Used for providers that require JS to be executed for inclusion of their widget. @@ -111,17 +119,11 @@ const OnRampProviderPopup = ({
- + e.preventDefault()} @@ -197,13 +199,10 @@ export default connect(({ modules, ui }: TRootStore) => ({ deposit_address: modules.cashier.onramp.deposit_address, is_dark_mode_on: ui.is_dark_mode_on, is_deposit_address_loading: modules.cashier.onramp.is_deposit_address_loading, - is_deposit_address_popover_open: modules.cashier.onramp.is_deposit_address_popover_open, is_requesting_widget_html: modules.cashier.onramp.is_requesting_widget_html, - onClickCopyDepositAddress: modules.cashier.onramp.onClickCopyDepositAddress, onClickDisclaimerContinue: modules.cashier.onramp.onClickDisclaimerContinue, onClickGoToDepositPage: modules.cashier.onramp.onClickGoToDepositPage, selected_provider: modules.cashier.onramp.selected_provider, - setDepositAddressRef: modules.cashier.onramp.setDepositAddressRef, setIsOnRampModalOpen: modules.cashier.onramp.setIsOnRampModalOpen, should_show_dialog: modules.cashier.onramp.should_show_dialog, should_show_widget: modules.cashier.onramp.should_show_widget, diff --git a/packages/cashier/src/stores/__tests__/on-ramp-store.spec.js b/packages/cashier/src/stores/__tests__/on-ramp-store.spec.js index 16da1071a5d2..1fa7b072a9b4 100644 --- a/packages/cashier/src/stores/__tests__/on-ramp-store.spec.js +++ b/packages/cashier/src/stores/__tests__/on-ramp-store.spec.js @@ -164,35 +164,6 @@ describe('OnRampStore', () => { expect(spyDisposeGetWidgetHtmlReaction).toBeCalledTimes(1); }); - it('should show and hide deposit address popover when deposit address is copied', async () => { - jest.useFakeTimers(); - jest.spyOn(document, 'createRange').mockImplementation(() => ({ - selectNodeContents: jest.fn(), - })); - jest.spyOn(window, 'window', 'get').mockImplementation(() => ({ - getSelection: () => ({ - addRange: jest.fn(), - removeAllRanges: jest.fn(), - }), - })); - Object.assign(navigator, { - clipboard: { - writeText: jest.fn(() => Promise.resolve()), - }, - }); - const spySetIsDepositAddressPopoverOpen = jest.spyOn(onramp_store, 'setIsDepositAddressPopoverOpen'); - onramp_store.onClickCopyDepositAddress(); - - expect(await spySetIsDepositAddressPopoverOpen).toHaveBeenCalledWith(true); - jest.runAllTimers(); - expect(setTimeout).toHaveBeenCalledTimes(1); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 500); - expect(await spySetIsDepositAddressPopoverOpen).toHaveBeenCalledWith(false); - - jest.restoreAllMocks(); - jest.useRealTimers(); - }); - it('should show widget when onClickDisclaimerContinue method was called', () => { onramp_store.onClickDisclaimerContinue(); @@ -277,7 +248,6 @@ describe('OnRampStore', () => { expect(onramp_store.api_error).toBeNull(); expect(onramp_store.deposit_address).toBeNull(); - expect(onramp_store.deposit_address_ref).toBeNull(); expect(onramp_store.is_deposit_address_loading).toBeTruthy(); expect(onramp_store.selected_provider).toBeNull(); expect(onramp_store.should_show_widget).toBeFalsy(); @@ -303,24 +273,12 @@ describe('OnRampStore', () => { expect(onramp_store.deposit_address).toBe('deposit address'); }); - it('should set deposit address ref', () => { - onramp_store.setDepositAddressRef('deposit address ref'); - - expect(onramp_store.deposit_address_ref).toBe('deposit address ref'); - }); - it('should change value of the variable is_deposit_address_loading', () => { onramp_store.setIsDepositAddressLoading(true); expect(onramp_store.is_deposit_address_loading).toBeTruthy(); }); - it('should change value of the variable is_deposit_address_popover_open', () => { - onramp_store.setIsDepositAddressPopoverOpen(true); - - expect(onramp_store.is_deposit_address_popover_open).toBeTruthy(); - }); - it('should change value of the variable is_onramp_modal_open', () => { onramp_store.setIsOnRampModalOpen(true); diff --git a/packages/cashier/src/stores/on-ramp-store.js b/packages/cashier/src/stores/on-ramp-store.js index 2d374a6b4dfe..37f2b67345b9 100644 --- a/packages/cashier/src/stores/on-ramp-store.js +++ b/packages/cashier/src/stores/on-ramp-store.js @@ -8,7 +8,6 @@ export default class OnRampStore extends BaseStore { @observable api_error = null; @observable deposit_address = null; @observable is_deposit_address_loading = true; - @observable is_deposit_address_popover_open = false; @observable is_onramp_modal_open = false; @observable is_requesting_widget_html = false; @observable.shallow onramp_providers = []; @@ -17,8 +16,6 @@ export default class OnRampStore extends BaseStore { @observable widget_error = null; @observable widget_html = null; - deposit_address_ref = null; - constructor({ WS, root_store }) { super({ root_store }); this.WS = WS; @@ -149,21 +146,6 @@ export default class OnRampStore extends BaseStore { } } - @action.bound - onClickCopyDepositAddress() { - const range = document.createRange(); - range.selectNodeContents(this.deposit_address_ref); - - const selections = window.getSelection(); - selections.removeAllRanges(); - selections.addRange(range); - - navigator.clipboard.writeText(this.deposit_address).then(() => { - this.setIsDepositAddressPopoverOpen(true); - setTimeout(() => this.setIsDepositAddressPopoverOpen(false), 500); - }); - } - @action.bound onClickDisclaimerContinue() { this.setShouldShowWidget(true); @@ -220,7 +202,6 @@ export default class OnRampStore extends BaseStore { resetPopup() { this.setApiError(null); this.setDepositAddress(null); - this.setDepositAddressRef(null); this.setIsDepositAddressLoading(true); this.setSelectedProvider(null); this.setShouldShowWidget(false); @@ -243,21 +224,11 @@ export default class OnRampStore extends BaseStore { this.deposit_address = deposit_address; } - @action.bound - setDepositAddressRef(ref) { - this.deposit_address_ref = ref; - } - @action.bound setIsDepositAddressLoading(is_loading) { this.is_deposit_address_loading = is_loading; } - @action.bound - setIsDepositAddressPopoverOpen(is_open) { - this.is_deposit_address_popover_open = is_open; - } - @action.bound setIsOnRampModalOpen(is_open) { this.is_onramp_modal_open = is_open; diff --git a/packages/cashier/src/utils/utility.js b/packages/cashier/src/utils/utility.js index 4ce3d73bd19c..df9c28765299 100644 --- a/packages/cashier/src/utils/utility.js +++ b/packages/cashier/src/utils/utility.js @@ -46,14 +46,6 @@ class PromiseClass { } } -const copyToClipboard = text => { - const textField = document.createElement('textarea'); - textField.innerText = text; - document.body.appendChild(textField); - textField.select(); - document.execCommand('copy'); - textField.remove(); -}; // TODO: [duplicate_code] - Move this to shared package // eu countries to support const eu_countries = [ @@ -104,4 +96,4 @@ const getAccountText = account => { return account_text; }; -export { copyToClipboard, createElement, getAccountText, getStaticHash, isEuCountry, PromiseClass, template }; +export { createElement, getAccountText, getStaticHash, isEuCountry, PromiseClass, template }; diff --git a/packages/components/src/components/clipboard/clipboard.jsx b/packages/components/src/components/clipboard/clipboard.jsx index a76fcdc43b93..b164beedc433 100644 --- a/packages/components/src/components/clipboard/clipboard.jsx +++ b/packages/components/src/components/clipboard/clipboard.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { useIsMounted } from '@deriv/shared'; import Popover from '../popover'; import Icon from '../icon'; +import { useCopyToClipboard } from '../../hooks'; const Clipboard = ({ text_copy, @@ -15,22 +16,13 @@ const Clipboard = ({ popover_props = {}, popoverAlignment = 'bottom', }) => { - const [is_copied, setIsCopied] = React.useState(false); + const [is_copied, copyToClipboard, setIsCopied] = useCopyToClipboard(); const isMounted = useIsMounted(); let timeout_clipboard = null; - const copyToClipboard = text => { - const textField = document.createElement('textarea'); - textField.innerText = text; - document.body.appendChild(textField); - textField.select(); - document.execCommand('copy'); - textField.remove(); - }; - const onClick = event => { copyToClipboard(text_copy); - setIsCopied(true); + timeout_clipboard = setTimeout(() => { if (isMounted()) { setIsCopied(false); @@ -44,31 +36,29 @@ const Clipboard = ({ }, [timeout_clipboard]); return ( - <> - - {is_copied && ( - - )} - {!is_copied && ( - - )} - - + + {is_copied && ( + + )} + {!is_copied && ( + + )} + ); }; Clipboard.propTypes = { diff --git a/packages/components/src/hooks/index.js b/packages/components/src/hooks/index.js index 6c7a7a434aa5..3c844f468bbc 100644 --- a/packages/components/src/hooks/index.js +++ b/packages/components/src/hooks/index.js @@ -9,3 +9,4 @@ export * from './use-deep-effect'; export * from './use-state-callback'; export * from './use-constructor'; export * from './use-safe-state'; +export * from './use-copy-to-clipboard'; diff --git a/packages/components/src/hooks/use-copy-to-clipboard.js b/packages/components/src/hooks/use-copy-to-clipboard.js new file mode 100644 index 000000000000..5a1f3a4f709f --- /dev/null +++ b/packages/components/src/hooks/use-copy-to-clipboard.js @@ -0,0 +1,18 @@ +import React from 'react'; + +export const useCopyToClipboard = () => { + const [is_copied, setIsCopied] = React.useState(false); + + const copyToClipboard = async text => { + if (!navigator?.clipboard) { + return false; + } + + await navigator.clipboard.writeText(text); + setIsCopied(true); + + return true; + }; + + return [is_copied, copyToClipboard, setIsCopied]; +}; diff --git a/packages/core/src/_common/utility.js b/packages/core/src/_common/utility.js index 93cb341b62eb..2ecf35655ae8 100644 --- a/packages/core/src/_common/utility.js +++ b/packages/core/src/_common/utility.js @@ -44,14 +44,6 @@ class PromiseClass { } } -const copyToClipboard = text => { - const textField = document.createElement('textarea'); - textField.innerText = text; - document.body.appendChild(textField); - textField.select(); - document.execCommand('copy'); - textField.remove(); -}; // TODO: [duplicate_code] - Move this to shared package // eu countries to support const eu_countries = [ @@ -109,5 +101,4 @@ module.exports = { isOptionsBlocked, isSyntheticsUnavailable, isMultipliersOnly, - copyToClipboard, }; From af9bc84b9c10342879ce03656cd1010f62fa03ae Mon Sep 17 00:00:00 2001 From: Bahar Date: Fri, 14 Oct 2022 12:49:55 +0800 Subject: [PATCH 2/2] add_error_handling --- .../src/hooks/use-copy-to-clipboard.js | 18 ------------- .../src/hooks/use-copy-to-clipboard.ts | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 18 deletions(-) delete mode 100644 packages/components/src/hooks/use-copy-to-clipboard.js create mode 100644 packages/components/src/hooks/use-copy-to-clipboard.ts diff --git a/packages/components/src/hooks/use-copy-to-clipboard.js b/packages/components/src/hooks/use-copy-to-clipboard.js deleted file mode 100644 index 5a1f3a4f709f..000000000000 --- a/packages/components/src/hooks/use-copy-to-clipboard.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; - -export const useCopyToClipboard = () => { - const [is_copied, setIsCopied] = React.useState(false); - - const copyToClipboard = async text => { - if (!navigator?.clipboard) { - return false; - } - - await navigator.clipboard.writeText(text); - setIsCopied(true); - - return true; - }; - - return [is_copied, copyToClipboard, setIsCopied]; -}; diff --git a/packages/components/src/hooks/use-copy-to-clipboard.ts b/packages/components/src/hooks/use-copy-to-clipboard.ts new file mode 100644 index 000000000000..d058d6616af0 --- /dev/null +++ b/packages/components/src/hooks/use-copy-to-clipboard.ts @@ -0,0 +1,26 @@ +import React from 'react'; + +type IsCopied = boolean; +type CopyFn = (text: string) => Promise; +type IsCopyFn = (flag: boolean) => void; + +export const useCopyToClipboard = (): [IsCopied, CopyFn, IsCopyFn] => { + const [is_copied, setIsCopied] = React.useState(false); + + const copyToClipboard = async (text: string) => { + if (!navigator?.clipboard) { + return false; + } + + try { + await navigator.clipboard.writeText(text); + setIsCopied(true); + return true; + } catch (error) { + setIsCopied(false); + return false; + } + }; + + return [is_copied, copyToClipboard, setIsCopied]; +};