Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor modals to wrap with ErrorBoundary and auto pass theme #182

Merged
merged 10 commits into from
Aug 21, 2024
1 change: 1 addition & 0 deletions packages/widget-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"lodash.debounce": "^4.0.8",
"pluralize": "^8.0.0",
"rc-virtual-list": "^3.14.5",
"react-error-boundary": "^4.0.13",
"react-shadow-scope": "^1.0.5",
"zod": "^3.23.8"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/widget-v2/src/components/AssetChainInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useTheme } from 'styled-components';
import { CogIcon } from '@/icons/CogIcon';
import { Button, GhostButton } from '@/components/Button';
import { useAtom } from 'jotai';
import { skipAssets } from '@/state/skip';
import { skipAssets } from '@/state/skipClient';
import { useUsdValue } from '@/utils/useUsdValue';
import { formatUSD } from '@/utils/intl';

Expand Down
82 changes: 76 additions & 6 deletions packages/widget-v2/src/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -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;
};

Expand All @@ -18,6 +24,7 @@ export const Modal = ({
drawer,
container,
onOpenChange,
stackedModal,
theme,
}: ModalProps) => {
const modal = useModal();
Expand All @@ -33,7 +40,7 @@ export const Modal = ({
<Dialog.Root open={modal.visible} onOpenChange={() => modal.remove()}>
<Dialog.Portal container={container}>
<ShadowDomAndProviders theme={theme}>
<StyledOverlay drawer={drawer}>
<StyledOverlay drawer={drawer} invisible={stackedModal}>
<StyledContent>{children}</StyledContent>
</StyledOverlay>
</ShadowDomAndProviders>
Expand All @@ -42,8 +49,71 @@ export const Modal = ({
);
};

const StyledOverlay = styled(Dialog.Overlay)<{ drawer?: boolean }>`
background: rgba(0 0 0 / 0.5);
export const createModal = <T extends ModalProps>(
component: ComponentType<T>
) => {
const Component = component;

const WrappedComponent = (props: T) => {
const [, setError] = useAtom(errorAtom);

return (
<Modal {...props}>
<ErrorBoundary fallback={null} onError={(error) => setError(error)}>
<Component {...props} />
</ErrorBoundary>
</Modal>
);
};

return NiceModal.create(WrappedComponent);
};

export const useModal = <T extends ModalProps>(
modal?: FC<T>,
initialArgs?: Partial<T>
) => {
const theme = useTheme();
const [numberOfModalsOpen, setNumberOfModalsOpen] = useAtom(
numberOfModalsOpenAtom
);

const modalInstance = modal
? useNiceModal(modal, initialArgs)
: useNiceModal();

const show = useCallback(
(showArgs?: Partial<T & ModalProps>) => {
setNumberOfModalsOpen((prev) => prev + 1);
modalInstance.show({
theme,
stackedModal: numberOfModalsOpen > 0,
...showArgs,
} as Partial<T>);
},
[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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,11 +11,11 @@ import {
import { WALLET_LIST } from '@/modals/WalletSelectorModal/WalletSelectorFlow';
import { Button } from '@/components/Button';
import { SmallText, Text } from '@/components/Typography';
import { destinationAssetAtom, destinationWalletAtom } from '@/state/swap';
import { destinationAssetAtom, destinationWalletAtom } from '@/state/swapPage';
import { useAtom } from 'jotai';
import { getChain } from '@/state/skip';
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);
Expand Down Expand Up @@ -62,7 +61,7 @@ export const ManualAddressModal = NiceModal.create((modalProps: ModalProps) => {
}, [manualWalletAddress]);

return (
<Modal {...modalProps}>
<>
{showManualAddressInput ? (
<StyledContainer gap={15}>
<RenderWalletListHeader
Expand Down Expand Up @@ -122,7 +121,7 @@ export const ManualAddressModal = NiceModal.create((modalProps: ModalProps) => {
onClickBackButton={() => modal.remove()}
/>
)}
</Modal>
</>
);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
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';
import { ChainWithAsset, ClientAsset, skipAssets } from '@/state/skip';
import { ChainWithAsset, ClientAsset, skipAssets } from '@/state/skipClient';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { VirtualList } from '@/components/VirtualList';
import {
Expand All @@ -19,7 +18,7 @@ export type TokenAndChainSelectorModalProps = ModalProps & {
asset?: Partial<ClientAsset>;
};

export const TokenAndChainSelectorModal = NiceModal.create(
export const TokenAndChainSelectorModal = createModal(
(modalProps: TokenAndChainSelectorModalProps) => {
const modal = useModal();
const { onSelect, chainsContainingAsset, asset } = modalProps;
Expand Down Expand Up @@ -78,34 +77,32 @@ export const TokenAndChainSelectorModal = NiceModal.create(
);

return (
<Modal {...modalProps}>
<StyledContainer>
<TokenAndChainSelectorModalSearchInput
onSearch={handleSearch}
asset={asset}
<StyledContainer>
<TokenAndChainSelectorModalSearchInput
onSearch={handleSearch}
asset={asset}
/>
{showSkeleton || (!filteredAssets && !filteredChains) ? (
<Column>
{Array.from({ length: 10 }, (_, index) => (
<Skeleton key={index} />
))}
</Column>
) : (
<VirtualList
listItems={filteredChains ?? filteredAssets ?? []}
height={530}
itemHeight={70}
renderItem={renderItem}
itemKey={(item) => {
if (isChainWithAsset(item)) {
return `${item.chain_id}${item.chain_name}`;
}
return `${item.chainID}${item.denom}`;
}}
/>
{showSkeleton || (!filteredAssets && !filteredChains) ? (
<Column>
{Array.from({ length: 10 }, (_, index) => (
<Skeleton key={index} />
))}
</Column>
) : (
<VirtualList
listItems={filteredChains ?? filteredAssets ?? []}
height={530}
itemHeight={70}
renderItem={renderItem}
itemKey={(item) => {
if (isChainWithAsset(item)) {
return `${item.chain_id}${item.chain_name}`;
}
return `${item.chainID}${item.denom}`;
}}
/>
)}
</StyledContainer>
</Modal>
)}
</StyledContainer>
);
}
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Row } from '@/components/Layout';
import { ModalRowItem } from '@/components/ModalRowItem';
import { SmallText, Text } from '@/components/Typography';
import { ChainWithAsset, ClientAsset } from '@/state/skip';
import { ChainWithAsset, ClientAsset } from '@/state/skipClient';
import { CircleSkeletonElement, SkeletonElement } from '@/components/Skeleton';
import { styled } from 'styled-components';
import { Chain } from '@chain-registry/types';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { SkipLogoIcon } from '@/icons/SkipLogoIcon';
import { SmallText } from '@/components/Typography';
import { SearchIcon } from '@/icons/SearchIcon';
import { StyledAssetLabel } from '@/components/AssetChainInput';
import { ClientAsset } from '@/state/skip';
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<Modal {...modalProps}>
<>
<SwapPageHeader
leftButton={{
label: 'Back',
Expand Down Expand Up @@ -75,7 +74,7 @@ export const TransactionHistoryModal = NiceModal.create(
)}
</StyledContainer>
<SwapPageFooter />
</Modal>
</>
);
}
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RouteResponse } from '@skip-go/client';
import { SmallText } from '@/components/Typography';
import { useAtom } from 'jotai';
import { skipAssets, getChain, ClientAsset } from '@/state/skip';
import { skipAssets, getChain, ClientAsset } from '@/state/skipClient';
import { Column, Row } from '@/components/Layout';
import styled, { useTheme } from 'styled-components';
import { getFormattedAssetAmount } from '@/utils/crypto';
Expand Down
Original file line number Diff line number Diff line change
@@ -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 & {
Expand All @@ -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 {...modalProps}>
<RenderWalletList
title="Connect wallet"
walletList={WALLET_LIST}
onSelect={onSelect}
onClickBackButton={() => modal.remove()}
/>
</Modal>
<RenderWalletList
title="Connect wallet"
walletList={WALLET_LIST}
onSelect={onSelect}
onClickBackButton={() => modal.remove()}
/>
);
}
);
26 changes: 26 additions & 0 deletions packages/widget-v2/src/pages/ErrorPage/ErrorPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { errorAtom } from '@/state/errorPage';
import { useResetAtom } from 'jotai/utils';

export const ErrorPage = ({
error,
componentStack,
resetErrorBoundary,
}: {
error?: Error;
componentStack?: string;
resetErrorBoundary?: () => void;
}) => {
const resetError = useResetAtom(errorAtom);

const handleReset = () => {
resetError();
resetErrorBoundary?.();
};

return (
<div>
error page
<button onClick={handleReset}>reset</button>
</div>
);
};
Loading
Loading