From b26ed5ebdff76728ce7d3c22416cc254c8f17bf9 Mon Sep 17 00:00:00 2001 From: Todd Kao Date: Wed, 21 Aug 2024 18:09:56 -0400 Subject: [PATCH] Refactor modals to wrap with ErrorBoundary and auto pass theme (#182) --- packages/widget-v2/src/components/Modal.tsx | 82 +++++++++++++- .../ManualAddressModal/ManualAddressModal.tsx | 9 +- .../TokenAndChainSelectorModal.tsx | 57 +++++----- .../TokenAndChainSelectorModalSearchInput.tsx | 2 +- .../TransactionHistoryModal.tsx | 9 +- .../WalletSelectorFlow.tsx | 19 ++-- .../src/pages/ErrorPage/ErrorPage.tsx | 17 ++- .../SwapExecutionPage/SwapExecutionPage.tsx | 12 +- .../widget-v2/src/pages/SwapPage/SwapPage.tsx | 9 +- .../src/pages/SwapPage/SwapPageSettings.tsx | 107 +++++++++--------- packages/widget-v2/src/state/modal.ts | 3 + packages/widget-v2/src/widget/Widget.tsx | 16 ++- 12 files changed, 203 insertions(+), 139 deletions(-) create mode 100644 packages/widget-v2/src/state/modal.ts diff --git a/packages/widget-v2/src/components/Modal.tsx b/packages/widget-v2/src/components/Modal.tsx index f2913a52d..a478209f0 100644 --- a/packages/widget-v2/src/components/Modal.tsx +++ b/packages/widget-v2/src/components/Modal.tsx @@ -1,15 +1,21 @@ -import { css, styled } from 'styled-components'; +import { css, styled, useTheme } from 'styled-components'; import * as Dialog from '@radix-ui/react-dialog'; import { ShadowDomAndProviders } from '@/widget/ShadowDomAndProviders'; -import { useModal } from '@ebay/nice-modal-react'; -import { useEffect } from 'react'; +import NiceModal, { useModal as useNiceModal } from '@ebay/nice-modal-react'; +import { ComponentType, FC, useCallback, useEffect, useMemo } from 'react'; import { PartialTheme } from '@/widget/theme'; +import { ErrorBoundary } from 'react-error-boundary'; +import { useAtom } from 'jotai'; +import { errorAtom } from '@/state/errorPage'; +import { numberOfModalsOpenAtom } from '@/state/modal'; + export type ModalProps = { children: React.ReactNode; drawer?: boolean; container?: HTMLElement; onOpenChange?: (open: boolean) => void; + stackedModal?: boolean; theme?: PartialTheme; }; @@ -18,6 +24,7 @@ export const Modal = ({ drawer, container, onOpenChange, + stackedModal, theme, }: ModalProps) => { const modal = useModal(); @@ -33,7 +40,7 @@ export const Modal = ({ modal.remove()}> - + {children} @@ -42,8 +49,71 @@ export const Modal = ({ ); }; -const StyledOverlay = styled(Dialog.Overlay)<{ drawer?: boolean }>` - background: rgba(0 0 0 / 0.5); +export const createModal = ( + component: ComponentType +) => { + const Component = component; + + const WrappedComponent = (props: T) => { + const [, setError] = useAtom(errorAtom); + + return ( + + setError(error)}> + + + + ); + }; + + return NiceModal.create(WrappedComponent); +}; + +export const useModal = ( + modal?: FC, + initialArgs?: Partial +) => { + const theme = useTheme(); + const [numberOfModalsOpen, setNumberOfModalsOpen] = useAtom( + numberOfModalsOpenAtom + ); + + const modalInstance = modal + ? useNiceModal(modal, initialArgs) + : useNiceModal(); + + const show = useCallback( + (showArgs?: Partial) => { + setNumberOfModalsOpen((prev) => prev + 1); + modalInstance.show({ + theme, + stackedModal: numberOfModalsOpen > 0, + ...showArgs, + } as Partial); + }, + [modalInstance, setNumberOfModalsOpen, theme, numberOfModalsOpen] + ); + + const remove = useCallback(() => { + setNumberOfModalsOpen((prev) => Math.max(0, prev - 1)); + modalInstance.remove(); + }, [modalInstance, setNumberOfModalsOpen]); + + return useMemo( + () => ({ + ...modalInstance, + show, + remove, + }), + [modalInstance, show, remove] + ); +}; + +const StyledOverlay = styled(Dialog.Overlay)<{ + drawer?: boolean; + invisible?: boolean; +}>` + ${({ invisible }) => (invisible ? '' : 'background: rgba(0 0 0 / 0.5);')} position: fixed; top: 0; left: 0; diff --git a/packages/widget-v2/src/modals/ManualAddressModal/ManualAddressModal.tsx b/packages/widget-v2/src/modals/ManualAddressModal/ManualAddressModal.tsx index 743fa32d2..d920ea7df 100644 --- a/packages/widget-v2/src/modals/ManualAddressModal/ManualAddressModal.tsx +++ b/packages/widget-v2/src/modals/ManualAddressModal/ManualAddressModal.tsx @@ -1,5 +1,4 @@ -import NiceModal, { useModal } from '@ebay/nice-modal-react'; -import { Modal, ModalProps } from '@/components/Modal'; +import { createModal, ModalProps, useModal } from '@/components/Modal'; import { Column, Row } from '@/components/Layout'; import { css, styled } from 'styled-components'; import { useCallback, useMemo, useState } from 'react'; @@ -16,7 +15,7 @@ import { destinationAssetAtom, destinationWalletAtom } from '@/state/swapPage'; import { useAtom } from 'jotai'; import { getChain } from '@/state/skipClient'; -export const ManualAddressModal = NiceModal.create((modalProps: ModalProps) => { +export const ManualAddressModal = createModal((modalProps: ModalProps) => { const { theme } = modalProps; const modal = useModal(); const [destinationAsset] = useAtom(destinationAssetAtom); @@ -62,7 +61,7 @@ export const ManualAddressModal = NiceModal.create((modalProps: ModalProps) => { }, [manualWalletAddress]); return ( - + <> {showManualAddressInput ? ( { onClickBackButton={() => modal.remove()} /> )} - + ); }); diff --git a/packages/widget-v2/src/modals/TokenAndChainSelectorModal/TokenAndChainSelectorModal.tsx b/packages/widget-v2/src/modals/TokenAndChainSelectorModal/TokenAndChainSelectorModal.tsx index 94e2e1f48..c81b2f831 100644 --- a/packages/widget-v2/src/modals/TokenAndChainSelectorModal/TokenAndChainSelectorModal.tsx +++ b/packages/widget-v2/src/modals/TokenAndChainSelectorModal/TokenAndChainSelectorModal.tsx @@ -1,5 +1,4 @@ -import NiceModal, { useModal } from '@ebay/nice-modal-react'; -import { Modal, ModalProps } from '@/components/Modal'; +import { createModal, ModalProps, useModal } from '@/components/Modal'; import { Column } from '@/components/Layout'; import { styled } from 'styled-components'; import { useAtom } from 'jotai'; @@ -19,7 +18,7 @@ export type TokenAndChainSelectorModalProps = ModalProps & { asset?: Partial; }; -export const TokenAndChainSelectorModal = NiceModal.create( +export const TokenAndChainSelectorModal = createModal( (modalProps: TokenAndChainSelectorModalProps) => { const modal = useModal(); const { onSelect, chainsContainingAsset, asset } = modalProps; @@ -78,34 +77,32 @@ export const TokenAndChainSelectorModal = NiceModal.create( ); return ( - - - + + {showSkeleton || (!filteredAssets && !filteredChains) ? ( + + {Array.from({ length: 10 }, (_, index) => ( + + ))} + + ) : ( + { + if (isChainWithAsset(item)) { + return `${item.chain_id}${item.chain_name}`; + } + return `${item.chainID}${item.denom}`; + }} /> - {showSkeleton || (!filteredAssets && !filteredChains) ? ( - - {Array.from({ length: 10 }, (_, index) => ( - - ))} - - ) : ( - { - if (isChainWithAsset(item)) { - return `${item.chain_id}${item.chain_name}`; - } - return `${item.chainID}${item.denom}`; - }} - /> - )} - - + )} + ); } ); diff --git a/packages/widget-v2/src/modals/TokenAndChainSelectorModal/TokenAndChainSelectorModalSearchInput.tsx b/packages/widget-v2/src/modals/TokenAndChainSelectorModal/TokenAndChainSelectorModalSearchInput.tsx index 52b13e58a..bc95503d2 100644 --- a/packages/widget-v2/src/modals/TokenAndChainSelectorModal/TokenAndChainSelectorModalSearchInput.tsx +++ b/packages/widget-v2/src/modals/TokenAndChainSelectorModal/TokenAndChainSelectorModalSearchInput.tsx @@ -7,9 +7,9 @@ import { SearchIcon } from '@/icons/SearchIcon'; import { StyledAssetLabel } from '@/components/AssetChainInput'; import { ClientAsset } from '@/state/skipClient'; import { LeftArrowIcon } from '@/icons/ArrowIcon'; -import { useModal } from '@ebay/nice-modal-react'; import { Button } from '@/components/Button'; import { Text } from '@/components/Typography'; +import { useModal } from '@/components/Modal'; type TokenAndChainSelectorModalSearchInputProps = { onSearch: (term: string) => void; diff --git a/packages/widget-v2/src/modals/TransactionHistoryModal/TransactionHistoryModal.tsx b/packages/widget-v2/src/modals/TransactionHistoryModal/TransactionHistoryModal.tsx index a1e73059f..447d26ccb 100644 --- a/packages/widget-v2/src/modals/TransactionHistoryModal/TransactionHistoryModal.tsx +++ b/packages/widget-v2/src/modals/TransactionHistoryModal/TransactionHistoryModal.tsx @@ -1,5 +1,4 @@ -import NiceModal from '@ebay/nice-modal-react'; -import { Modal, ModalProps } from '@/components/Modal'; +import { createModal, ModalProps } from '@/components/Modal'; import { Column } from '@/components/Layout'; import { styled } from 'styled-components'; import { SwapPageHeader } from '@/pages/SwapPage/SwapPageHeader'; @@ -21,13 +20,13 @@ export type TransactionHistoryModalProps = ModalProps & { txHistory: TxHistoryItem[]; }; -export const TransactionHistoryModal = NiceModal.create( +export const TransactionHistoryModal = createModal( ({ txHistory, ...modalProps }: TransactionHistoryModalProps) => { const [itemIndexToShowDetail, setItemIndexToShowDetail] = useState< number | undefined >(); return ( - + <> - + ); } ); diff --git a/packages/widget-v2/src/modals/WalletSelectorModal/WalletSelectorFlow.tsx b/packages/widget-v2/src/modals/WalletSelectorModal/WalletSelectorFlow.tsx index 8fec1823b..61c8022de 100644 --- a/packages/widget-v2/src/modals/WalletSelectorModal/WalletSelectorFlow.tsx +++ b/packages/widget-v2/src/modals/WalletSelectorModal/WalletSelectorFlow.tsx @@ -1,5 +1,4 @@ -import NiceModal, { useModal } from '@ebay/nice-modal-react'; -import { Modal, ModalProps } from '@/components/Modal'; +import { createModal, ModalProps, useModal } from '@/components/Modal'; import { RenderWalletList, Wallet } from '@/components/RenderWalletList'; export type WalletSelectorModalProps = ModalProps & { @@ -26,20 +25,18 @@ export const WALLET_LIST: Wallet[] = [ }, ]; -export const WalletSelectorModal = NiceModal.create( +export const WalletSelectorModal = createModal( (modalProps: WalletSelectorModalProps) => { const { onSelect } = modalProps; const modal = useModal(); return ( - - modal.remove()} - /> - + modal.remove()} + /> ); } ); diff --git a/packages/widget-v2/src/pages/ErrorPage/ErrorPage.tsx b/packages/widget-v2/src/pages/ErrorPage/ErrorPage.tsx index a0e672a40..264f7d2b8 100644 --- a/packages/widget-v2/src/pages/ErrorPage/ErrorPage.tsx +++ b/packages/widget-v2/src/pages/ErrorPage/ErrorPage.tsx @@ -1,13 +1,26 @@ import { errorAtom } from '@/state/errorPage'; import { useResetAtom } from 'jotai/utils'; -export const ErrorPage = () => { +export const ErrorPage = ({ + error, + componentStack, + resetErrorBoundary, +}: { + error?: Error; + componentStack?: string; + resetErrorBoundary?: () => void; +}) => { const resetError = useResetAtom(errorAtom); + const handleReset = () => { + resetError(); + resetErrorBoundary?.(); + }; + return (
error page - +
); }; diff --git a/packages/widget-v2/src/pages/SwapExecutionPage/SwapExecutionPage.tsx b/packages/widget-v2/src/pages/SwapExecutionPage/SwapExecutionPage.tsx index 2dba7da34..c790053a9 100644 --- a/packages/widget-v2/src/pages/SwapExecutionPage/SwapExecutionPage.tsx +++ b/packages/widget-v2/src/pages/SwapExecutionPage/SwapExecutionPage.tsx @@ -4,7 +4,6 @@ import { SwapPageFooter } from '@/pages/SwapPage/SwapPageFooter'; import { SwapPageHeader } from '@/pages/SwapPage/SwapPageHeader'; import { useEffect, useMemo, useState } from 'react'; import { ICONS } from '@/icons'; -import { useModal } from '@ebay/nice-modal-react'; import { ManualAddressModal } from '@/modals/ManualAddressModal/ManualAddressModal'; import styled, { useTheme } from 'styled-components'; import { useAtom } from 'jotai'; @@ -18,6 +17,7 @@ import { SmallText } from '@/components/Typography'; import { SignatureIcon } from '@/icons/SignatureIcon'; import pluralize from 'pluralize'; import operations from './operations.json'; +import { useModal } from '@/components/Modal'; enum SwapExecutionState { destinationAddressUnset, @@ -92,11 +92,7 @@ export const SwapExecutionPage = () => { - modal.show({ - theme, - }) - } + onClick={() => modal.show()} /> ); case SwapExecutionState.unconfirmed: @@ -133,9 +129,7 @@ export const SwapExecutionPage = () => { const SwapExecutionPageRoute = simpleRoute ? withBoundProps(SwapExecutionPageRouteSimple, { onClickEditDestinationWallet: () => { - modal.show({ - theme, - }); + modal.show(); }, }) : SwapExecutionPageRouteDetailed; diff --git a/packages/widget-v2/src/pages/SwapPage/SwapPage.tsx b/packages/widget-v2/src/pages/SwapPage/SwapPage.tsx index 44013787f..78404632f 100644 --- a/packages/widget-v2/src/pages/SwapPage/SwapPage.tsx +++ b/packages/widget-v2/src/pages/SwapPage/SwapPage.tsx @@ -1,6 +1,4 @@ -import { useTheme } from 'styled-components'; import { useCallback, useMemo, useState } from 'react'; -import { useModal } from '@ebay/nice-modal-react'; import { useAtom } from 'jotai'; import { AssetChainInput } from '@/components/AssetChainInput'; import { Column } from '@/components/Layout'; @@ -14,11 +12,11 @@ import { SwapPageSettings } from './SwapPageSettings'; import { SwapPageFooter } from './SwapPageFooter'; import { SwapPageBridge } from './SwapPageBridge'; import { SwapPageHeader } from './SwapPageHeader'; +import { useModal } from '@/components/Modal'; const sourceAssetBalance = 125; export const SwapPage = () => { - const theme = useTheme(); const [container, setContainer] = useState(); const [drawerOpen, setDrawerOpen] = useState(false); const [sourceAsset, setSourceAsset] = useAtom(sourceAssetAtom); @@ -42,7 +40,6 @@ export const SwapPage = () => { const handleChangeSourceAsset = useCallback(() => { tokenAndChainSelectorFlow.show({ - theme, onSelect: (asset) => { setSourceAsset((old) => ({ ...old, @@ -57,7 +54,6 @@ export const SwapPage = () => { if (!chainsContainingSourceAsset) return; return tokenAndChainSelectorFlow.show({ - theme, onSelect: (asset) => { setSourceAsset((old) => ({ ...old, @@ -74,7 +70,6 @@ export const SwapPage = () => { const handleChangeDestinationAsset = useCallback(() => { tokenAndChainSelectorFlow.show({ - theme, onSelect: (asset) => { setDestinationAsset((old) => ({ ...old, @@ -89,7 +84,6 @@ export const SwapPage = () => { if (!chainsContainingDestinationAsset) return; return tokenAndChainSelectorFlow.show({ - theme, onSelect: (asset) => { setDestinationAsset((old) => ({ ...old, @@ -153,7 +147,6 @@ export const SwapPage = () => { showRouteInfo onClick={() => swapFlowSettings.show({ - theme, drawer: true, container, onOpenChange: (open: boolean) => diff --git a/packages/widget-v2/src/pages/SwapPage/SwapPageSettings.tsx b/packages/widget-v2/src/pages/SwapPage/SwapPageSettings.tsx index a23fa573a..98d862534 100644 --- a/packages/widget-v2/src/pages/SwapPage/SwapPageSettings.tsx +++ b/packages/widget-v2/src/pages/SwapPage/SwapPageSettings.tsx @@ -1,7 +1,6 @@ import { css, styled } from 'styled-components'; -import { Modal, ModalProps } from '@/components/Modal'; +import { createModal, ModalProps } from '@/components/Modal'; import { Column, Row } from '@/components/Layout'; -import NiceModal from '@ebay/nice-modal-react'; import { SmallText } from '@/components/Typography'; import { RouteArrow } from '@/icons/RouteArrow'; import { SwapPageFooterItems } from './SwapPageFooter'; @@ -31,65 +30,61 @@ const bridgeFee = '0.001 XYZ ($0.1)'; const selectedOption = SLIPPAGE_OPTIONS[0]; const route = ['COSMOS', 'OSMOSIS', 'AXELAR']; -export const SwapPageSettings = NiceModal.create((modalProps: ModalProps) => { +export const SwapPageSettings = createModal((modalProps: ModalProps) => { return ( - - - - - Route - - {route.map((_path, index) => ( - <> - - {index !== route.length - 1 && ( - - )} - - ))} - + + + + Route + + {route.map((_path, index) => ( + <> + + {index !== route.length - 1 && ( + + )} + + ))} - - Max Slippage - - {SLIPPAGE_OPTIONS.map(({ label }) => ( - - {label} - - ))} - + + + Max Slippage + + {SLIPPAGE_OPTIONS.map(({ label }) => ( + + {label} + + ))} - + + - - - Total Gas - {totalGas} - - - Router Fee - {routerFee} - - - Bridge Fee - {bridgeFee} - - + + + Total Gas + {totalGas} + + + Router Fee + {routerFee} + + + Bridge Fee + {bridgeFee} + + - - - - - + + + + ); }); diff --git a/packages/widget-v2/src/state/modal.ts b/packages/widget-v2/src/state/modal.ts new file mode 100644 index 000000000..89476e04e --- /dev/null +++ b/packages/widget-v2/src/state/modal.ts @@ -0,0 +1,3 @@ +import { atomWithReset } from 'jotai/utils'; + +export const numberOfModalsOpenAtom = atomWithReset(0); diff --git a/packages/widget-v2/src/widget/Widget.tsx b/packages/widget-v2/src/widget/Widget.tsx index db7b8fc5b..225f82a65 100644 --- a/packages/widget-v2/src/widget/Widget.tsx +++ b/packages/widget-v2/src/widget/Widget.tsx @@ -1,10 +1,12 @@ import { ShadowDomAndProviders } from './ShadowDomAndProviders'; -import NiceModal, { useModal } from '@ebay/nice-modal-react'; +import NiceModal from '@ebay/nice-modal-react'; import { styled } from 'styled-components'; -import { Modal } from '@/components/Modal'; +import { createModal, useModal } from '@/components/Modal'; import { cloneElement, ReactElement } from 'react'; import { PartialTheme } from './theme'; import { Router } from './Router'; +import { useResetAtom } from 'jotai/utils'; +import { numberOfModalsOpenAtom } from '@/state/modal'; export type SwapWidgetProps = { theme?: PartialTheme; @@ -40,12 +42,14 @@ export const ShowSwapWidget = ({ button = , ...props }: ShowSwapWidget) => { - const modal = useModal(NiceModal.create(Modal)); + const modal = useModal( + createModal(() => ) + ); + const resetNumberOfModalsOpen = useResetAtom(numberOfModalsOpenAtom); const handleClick = () => { - modal.show({ - children: , - }); + resetNumberOfModalsOpen(); + modal.show(); }; const Element = cloneElement(button, { onClick: handleClick });