From 672aa0e31b1ab600e83911084f99f00a5dbb2636 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 16 Jul 2024 15:02:55 +0800 Subject: [PATCH 1/2] refactor(experience): support and apply modal loading state --- .../Providers/ConfirmModalProvider/index.tsx | 110 ++++++--- .../ConfirmModalProvider/indext.test.tsx | 210 +++++++++++++----- .../src/components/ConfirmModal/AcModal.tsx | 2 + .../components/ConfirmModal/MobileModal.tsx | 8 +- .../src/components/ConfirmModal/type.ts | 1 + .../use-identifier-error-alert.ts | 4 +- .../use-link-social-confirm-modal.ts | 16 +- .../use-register-flow-code-verification.ts | 36 ++- .../use-sign-in-flow-code-verification.ts | 40 ++-- .../experience/src/hooks/use-confirm-modal.ts | 46 +++- packages/experience/src/hooks/use-terms.ts | 4 +- .../src/pages/Continue/SetPassword/index.tsx | 4 +- .../src/pages/RegisterPassword/index.tsx | 4 +- .../src/pages/ResetPassword/index.tsx | 4 +- 14 files changed, 335 insertions(+), 154 deletions(-) diff --git a/packages/experience/src/Providers/ConfirmModalProvider/index.tsx b/packages/experience/src/Providers/ConfirmModalProvider/index.tsx index 1a2bd1377c1..97b2154c2c4 100644 --- a/packages/experience/src/Providers/ConfirmModalProvider/index.tsx +++ b/packages/experience/src/Providers/ConfirmModalProvider/index.tsx @@ -6,30 +6,37 @@ import type { ModalProps } from '@/components/ConfirmModal'; import { WebModal, MobileModal } from '@/components/ConfirmModal'; import usePlatform from '@/hooks/use-platform'; -export type ModalContentRenderProps = { - confirm: (data?: unknown) => void; - cancel: (data?: unknown) => void; -}; - type ConfirmModalType = 'alert' | 'confirm'; type ConfirmModalState = Omit & { - ModalContent: string | ((props: ModalContentRenderProps) => Nullable); + ModalContent: string | (() => Nullable); type: ConfirmModalType; + isConfirmLoading?: boolean; }; -type ConfirmModalProps = Omit & { type?: ConfirmModalType }; +/** + * Props for promise-based modal usage + */ +type PromiseConfirmModalProps = Omit & { + type?: ConfirmModalType; +}; + +/** + * Props for callback-based modal usage + */ +export type CallbackConfirmModalProps = PromiseConfirmModalProps & { + onConfirm?: () => Promise | void; + onCancel?: () => void; +}; type ConfirmModalContextType = { - show: (props: ConfirmModalProps) => Promise<[boolean, unknown?]>; - confirm: (data?: unknown) => void; - cancel: (data?: unknown) => void; + showPromise: (props: PromiseConfirmModalProps) => Promise<[boolean, unknown?]>; + showCallback: (props: CallbackConfirmModalProps) => void; }; export const ConfirmModalContext = createContext({ - show: async () => [true], - confirm: noop, - cancel: noop, + showPromise: async () => [true], + showCallback: noop, }); type Props = { @@ -40,49 +47,86 @@ const defaultModalState: ConfirmModalState = { isOpen: false, type: 'confirm', ModalContent: () => null, + isConfirmLoading: false, }; +/** + * ConfirmModalProvider component + * + * This component provides a context for managing confirm modals throughout the application. + * It supports both promise-based and callback-based usage patterns. see `usePromiseConfirmModal` and `useConfirmModal` hooks. + */ const ConfirmModalProvider = ({ children }: Props) => { const [modalState, setModalState] = useState(defaultModalState); const resolver = useRef<(value: [result: boolean, data?: unknown]) => void>(); + const callbackRef = useRef<{ + onConfirm?: () => Promise | void; + onCancel?: () => void; + }>({}); const { isMobile } = usePlatform(); const ConfirmModal = isMobile ? MobileModal : WebModal; - const handleShow = useCallback(async ({ type = 'confirm', ...props }: ConfirmModalProps) => { - resolver.current?.([false]); + const handleShowPromise = useCallback( + async ({ type = 'confirm', ...props }: PromiseConfirmModalProps) => { + resolver.current?.([false]); + + setModalState({ + isOpen: true, + type, + isConfirmLoading: false, + ...props, + }); + + return new Promise<[result: boolean, data?: unknown]>((resolve) => { + // eslint-disable-next-line @silverhand/fp/no-mutation + resolver.current = resolve; + }); + }, + [] + ); + + const handleShowCallback = useCallback( + ({ type = 'confirm', onConfirm, onCancel, ...props }: CallbackConfirmModalProps) => { + resolver.current?.([false]); - setModalState({ - isOpen: true, - type, - ...props, - }); + setModalState({ + isOpen: true, + type, + isConfirmLoading: false, + ...props, + }); - return new Promise<[result: boolean, data?: unknown]>((resolve) => { // eslint-disable-next-line @silverhand/fp/no-mutation - resolver.current = resolve; - }); - }, []); + callbackRef.current = { onConfirm, onCancel }; + }, + [] + ); - const handleConfirm = useCallback((data?: unknown) => { + const handleConfirm = useCallback(async (data?: unknown) => { + if (callbackRef.current.onConfirm) { + setModalState((previous) => ({ ...previous, isConfirmLoading: true })); + await callbackRef.current.onConfirm(); + setModalState((previous) => ({ ...previous, isConfirmLoading: false })); + } resolver.current?.([true, data]); setModalState(defaultModalState); }, []); const handleCancel = useCallback((data?: unknown) => { + callbackRef.current.onCancel?.(); resolver.current?.([false, data]); setModalState(defaultModalState); }, []); const contextValue = useMemo( () => ({ - show: handleShow, - confirm: handleConfirm, - cancel: handleCancel, + showPromise: handleShowPromise, + showCallback: handleShowCallback, }), - [handleCancel, handleConfirm, handleShow] + [handleShowPromise, handleShowCallback] ); const { ModalContent, type, ...restProps } = modalState; @@ -95,7 +139,7 @@ const ConfirmModalProvider = ({ children }: Props) => { onConfirm={ type === 'confirm' ? () => { - handleConfirm(); + void handleConfirm(); } : undefined } @@ -103,11 +147,7 @@ const ConfirmModalProvider = ({ children }: Props) => { handleCancel(); }} > - {typeof ModalContent === 'string' ? ( - ModalContent - ) : ( - - )} + {typeof ModalContent === 'string' ? ModalContent : } ); diff --git a/packages/experience/src/Providers/ConfirmModalProvider/indext.test.tsx b/packages/experience/src/Providers/ConfirmModalProvider/indext.test.tsx index 16072159eb3..acb80bd8064 100644 --- a/packages/experience/src/Providers/ConfirmModalProvider/indext.test.tsx +++ b/packages/experience/src/Providers/ConfirmModalProvider/indext.test.tsx @@ -1,15 +1,15 @@ import { render, fireEvent, waitFor } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; -import { useConfirmModal } from '@/hooks/use-confirm-modal'; +import { useConfirmModal, usePromiseConfirmModal } from '@/hooks/use-confirm-modal'; import ConfirmModalProvider from '.'; const confirmHandler = jest.fn(); const cancelHandler = jest.fn(); -const ConfirmModalTestComponent = () => { - const { show } = useConfirmModal(); +const PromiseConfirmModalTestComponent = () => { + const { show } = usePromiseConfirmModal(); const onClick = async () => { const [result] = await show({ ModalContent: 'confirm modal content' }); @@ -26,82 +26,178 @@ const ConfirmModalTestComponent = () => { return ; }; -describe('confirm modal provider', () => { - it('render confirm modal', async () => { - const { queryByText, getByText } = render( - - - - ); - - const trigger = getByText('show modal'); +const CallbackConfirmModalTestComponent = () => { + const { show } = useConfirmModal(); - act(() => { - fireEvent.click(trigger); + const onClick = () => { + show({ + ModalContent: 'confirm modal content', + onConfirm: confirmHandler, + onCancel: cancelHandler, }); + }; + + return ; +}; - await waitFor(() => { - expect(queryByText('confirm modal content')).not.toBeNull(); - expect(queryByText('action.confirm')).not.toBeNull(); - expect(queryByText('action.cancel')).not.toBeNull(); +describe('confirm modal provider', () => { + describe('promise confirm modal', () => { + it('render confirm modal', async () => { + const { queryByText, getByText } = render( + + + + ); + + const trigger = getByText('show modal'); + + act(() => { + fireEvent.click(trigger); + }); + + await waitFor(() => { + expect(queryByText('confirm modal content')).not.toBeNull(); + expect(queryByText('action.confirm')).not.toBeNull(); + expect(queryByText('action.cancel')).not.toBeNull(); + }); }); - }); - it('confirm callback of confirm modal', async () => { - const { queryByText, getByText } = render( - - - - ); + it('confirm callback of confirm modal', async () => { + const { queryByText, getByText } = render( + + + + ); - const trigger = getByText('show modal'); + const trigger = getByText('show modal'); - act(() => { - fireEvent.click(trigger); - }); + act(() => { + fireEvent.click(trigger); + }); - await waitFor(() => { - expect(queryByText('confirm modal content')).not.toBeNull(); - expect(queryByText('action.confirm')).not.toBeNull(); - }); + await waitFor(() => { + expect(queryByText('confirm modal content')).not.toBeNull(); + expect(queryByText('action.confirm')).not.toBeNull(); + }); - const confirm = getByText('action.confirm'); + const confirm = getByText('action.confirm'); - act(() => { - fireEvent.click(confirm); - }); + act(() => { + fireEvent.click(confirm); + }); - await waitFor(() => { - expect(confirmHandler).toBeCalled(); + await waitFor(() => { + expect(confirmHandler).toBeCalled(); + }); }); - }); - it('cancel callback of confirm modal', async () => { - const { queryByText, getByText } = render( - - - - ); + it('cancel callback of confirm modal', async () => { + const { queryByText, getByText } = render( + + + + ); + + const trigger = getByText('show modal'); - const trigger = getByText('show modal'); + act(() => { + fireEvent.click(trigger); + }); - act(() => { - fireEvent.click(trigger); + await waitFor(() => { + expect(queryByText('confirm modal content')).not.toBeNull(); + expect(queryByText('action.cancel')).not.toBeNull(); + }); + + const cancel = getByText('action.cancel'); + + act(() => { + fireEvent.click(cancel); + }); + + await waitFor(() => { + expect(cancelHandler).toBeCalled(); + }); }); + }); - await waitFor(() => { - expect(queryByText('confirm modal content')).not.toBeNull(); - expect(queryByText('action.cancel')).not.toBeNull(); + describe('callback confirm modal', () => { + it('render confirm modal', async () => { + const { queryByText, getByText } = render( + + + + ); + + const trigger = getByText('show modal'); + + act(() => { + fireEvent.click(trigger); + }); + + await waitFor(() => { + expect(queryByText('confirm modal content')).not.toBeNull(); + expect(queryByText('action.confirm')).not.toBeNull(); + expect(queryByText('action.cancel')).not.toBeNull(); + }); }); - const cancel = getByText('action.cancel'); + it('confirm callback of confirm modal', async () => { + const { queryByText, getByText } = render( + + + + ); + + const trigger = getByText('show modal'); + + act(() => { + fireEvent.click(trigger); + }); + + await waitFor(() => { + expect(queryByText('confirm modal content')).not.toBeNull(); + expect(queryByText('action.confirm')).not.toBeNull(); + }); + + const confirm = getByText('action.confirm'); - act(() => { - fireEvent.click(cancel); + act(() => { + fireEvent.click(confirm); + }); + + await waitFor(() => { + expect(confirmHandler).toBeCalled(); + }); }); - await waitFor(() => { - expect(cancelHandler).toBeCalled(); + it('cancel callback of confirm modal', async () => { + const { queryByText, getByText } = render( + + + + ); + + const trigger = getByText('show modal'); + + act(() => { + fireEvent.click(trigger); + }); + + await waitFor(() => { + expect(queryByText('confirm modal content')).not.toBeNull(); + expect(queryByText('action.cancel')).not.toBeNull(); + }); + + const cancel = getByText('action.cancel'); + + act(() => { + fireEvent.click(cancel); + }); + + await waitFor(() => { + expect(cancelHandler).toBeCalled(); + }); }); }); }); diff --git a/packages/experience/src/components/ConfirmModal/AcModal.tsx b/packages/experience/src/components/ConfirmModal/AcModal.tsx index 0f7ce45977e..ce9a1762e30 100644 --- a/packages/experience/src/components/ConfirmModal/AcModal.tsx +++ b/packages/experience/src/components/ConfirmModal/AcModal.tsx @@ -16,6 +16,7 @@ import type { ModalProps } from './type'; const AcModal = ({ className, isOpen = false, + isConfirmLoading = false, children, cancelText = 'action.cancel', confirmText = 'action.confirm', @@ -69,6 +70,7 @@ const AcModal = ({ title={confirmText} i18nProps={confirmTextI18nProps} size="small" + isLoading={isConfirmLoading} onClick={onConfirm} /> )} diff --git a/packages/experience/src/components/ConfirmModal/MobileModal.tsx b/packages/experience/src/components/ConfirmModal/MobileModal.tsx index 9b398da3cb7..a22b105abb2 100644 --- a/packages/experience/src/components/ConfirmModal/MobileModal.tsx +++ b/packages/experience/src/components/ConfirmModal/MobileModal.tsx @@ -11,6 +11,7 @@ import type { ModalProps } from './type'; const MobileModal = ({ className, isOpen = false, + isConfirmLoading = false, children, cancelText = 'action.cancel', confirmText = 'action.confirm', @@ -38,7 +39,12 @@ const MobileModal = ({ onClick={onClose} /> {onConfirm && ( -