diff --git a/.changeset/happy-snakes-change.md b/.changeset/happy-snakes-change.md new file mode 100644 index 00000000..7e04e6df --- /dev/null +++ b/.changeset/happy-snakes-change.md @@ -0,0 +1,5 @@ +--- +"@fuels/react": minor +--- + +Add FuelChainProvider diff --git a/.changeset/shiny-coats-yawn.md b/.changeset/shiny-coats-yawn.md new file mode 100644 index 00000000..bc604908 --- /dev/null +++ b/.changeset/shiny-coats-yawn.md @@ -0,0 +1,5 @@ +--- +"@fuels/react": patch +--- + +Moved icons and global stylings to HOC diff --git a/examples/react-app/src/main.tsx b/examples/react-app/src/main.tsx index 3f6aaf9c..faff7e6e 100644 --- a/examples/react-app/src/main.tsx +++ b/examples/react-app/src/main.tsx @@ -57,6 +57,7 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ; diff --git a/packages/react/src/ui/Connect/icons/BackIcon.tsx b/packages/react/src/icons/BackIcon.tsx similarity index 91% rename from packages/react/src/ui/Connect/icons/BackIcon.tsx rename to packages/react/src/icons/BackIcon.tsx index 10258e95..9b81ac30 100644 --- a/packages/react/src/ui/Connect/icons/BackIcon.tsx +++ b/packages/react/src/icons/BackIcon.tsx @@ -1,4 +1,4 @@ -import type { SvgIconProps } from '../../types'; +import type { SvgIconProps } from '../types'; export function BackIcon({ size, ...props }: SvgIconProps) { return ( diff --git a/packages/react/src/ui/Connect/icons/CloseIcon.tsx b/packages/react/src/icons/CloseIcon.tsx similarity index 91% rename from packages/react/src/ui/Connect/icons/CloseIcon.tsx rename to packages/react/src/icons/CloseIcon.tsx index db572cd0..d69b4cc1 100644 --- a/packages/react/src/ui/Connect/icons/CloseIcon.tsx +++ b/packages/react/src/icons/CloseIcon.tsx @@ -1,4 +1,4 @@ -import type { SvgIconProps } from '../../../ui/types'; +import type { SvgIconProps } from '../types'; export function CloseIcon({ size, ...props }: SvgIconProps) { return ( diff --git a/packages/react/src/ui/Connect/icons/FuelWalletDevelopmentIcon.tsx b/packages/react/src/icons/FuelWalletDevelopmentIcon.tsx similarity index 95% rename from packages/react/src/ui/Connect/icons/FuelWalletDevelopmentIcon.tsx rename to packages/react/src/icons/FuelWalletDevelopmentIcon.tsx index e0b8b129..ba9f1df5 100644 --- a/packages/react/src/ui/Connect/icons/FuelWalletDevelopmentIcon.tsx +++ b/packages/react/src/icons/FuelWalletDevelopmentIcon.tsx @@ -1,4 +1,4 @@ -import type { SvgIconProps } from '../../types'; +import type { SvgIconProps } from '../types'; export function FuelWalletDevelopmentIcon({ size, ...props }: SvgIconProps) { return ( diff --git a/packages/react/src/ui/Connect/icons/FuelWalletIcon.tsx b/packages/react/src/icons/FuelWalletIcon.tsx similarity index 95% rename from packages/react/src/ui/Connect/icons/FuelWalletIcon.tsx rename to packages/react/src/icons/FuelWalletIcon.tsx index 11a7a1b2..562bf4a0 100644 --- a/packages/react/src/ui/Connect/icons/FuelWalletIcon.tsx +++ b/packages/react/src/icons/FuelWalletIcon.tsx @@ -1,4 +1,4 @@ -import type { SvgIconProps } from '../../types'; +import type { SvgIconProps } from '../types'; export function FuelWalletIcon({ size, ...props }: SvgIconProps) { return ( diff --git a/packages/react/src/ui/Connect/icons/FueletIcon.tsx b/packages/react/src/icons/FueletIcon.tsx similarity index 94% rename from packages/react/src/ui/Connect/icons/FueletIcon.tsx rename to packages/react/src/icons/FueletIcon.tsx index 00ab230d..0a9358d3 100644 --- a/packages/react/src/ui/Connect/icons/FueletIcon.tsx +++ b/packages/react/src/icons/FueletIcon.tsx @@ -1,4 +1,4 @@ -import type { SvgIconProps } from '../../../ui/types'; +import type { SvgIconProps } from '../types'; export function FueletIcon({ theme, size, ...props }: SvgIconProps) { return ( diff --git a/packages/react/src/ui/Connect/icons/InfoCircleIcon.tsx b/packages/react/src/icons/InfoCircleIcon.tsx similarity index 95% rename from packages/react/src/ui/Connect/icons/InfoCircleIcon.tsx rename to packages/react/src/icons/InfoCircleIcon.tsx index a56541e0..68887d40 100644 --- a/packages/react/src/ui/Connect/icons/InfoCircleIcon.tsx +++ b/packages/react/src/icons/InfoCircleIcon.tsx @@ -1,4 +1,4 @@ -import type { SvgIconProps } from '../../../ui/types'; +import type { SvgIconProps } from '../types'; export function InfoCircleIcon({ theme, diff --git a/packages/react/src/ui/Connect/icons/NoFundIcon.tsx b/packages/react/src/icons/NoFundIcon.tsx similarity index 97% rename from packages/react/src/ui/Connect/icons/NoFundIcon.tsx rename to packages/react/src/icons/NoFundIcon.tsx index cdfc2241..8d08b022 100644 --- a/packages/react/src/ui/Connect/icons/NoFundIcon.tsx +++ b/packages/react/src/icons/NoFundIcon.tsx @@ -1,4 +1,4 @@ -import type { SvgIconProps } from '../../types'; +import type { SvgIconProps } from '../types'; export function NoFundIcon({ size, ...props }: SvgIconProps) { return ( diff --git a/packages/react/src/ui/Connect/components/Spinner/Spinner.tsx b/packages/react/src/icons/Spinner.tsx similarity index 100% rename from packages/react/src/ui/Connect/components/Spinner/Spinner.tsx rename to packages/react/src/icons/Spinner.tsx diff --git a/packages/react/src/providers/FuelChainProvider.tsx b/packages/react/src/providers/FuelChainProvider.tsx new file mode 100644 index 00000000..92260280 --- /dev/null +++ b/packages/react/src/providers/FuelChainProvider.tsx @@ -0,0 +1,24 @@ +import type { Network } from 'fuels'; +import { createContext, useContext } from 'react'; + +const context = createContext(null); + +/** + * A hook that returns the target chain id to be enforced on the active provider. + * + * @examples + * ```ts + * const { chainId } = useFuelChain(); + * console.log(chainId); + * ``` + */ +export function useFuelChain() { + const contextData = useContext(context); + + if (contextData === null) { + throw new Error('useFuelChain must be used within a FuelChainProvider'); + } + return { chainId: contextData }; +} + +export const FuelChainProvider = context.Provider; diff --git a/packages/react/src/providers/FuelProvider.tsx b/packages/react/src/providers/FuelProvider.tsx index 33599079..ca134dbf 100644 --- a/packages/react/src/providers/FuelProvider.tsx +++ b/packages/react/src/providers/FuelProvider.tsx @@ -2,7 +2,9 @@ import type { FuelConfig } from 'fuels'; import { Connect } from '../ui/Connect'; -import { FuelHooksProvider } from './FuelHooksProvider'; +import { FuelChainProvider } from '../providers/FuelChainProvider'; +import { NetworkMonitor } from '../ui/NetworkMonitor'; +import { FuelHooksProvider, useFuel } from './FuelHooksProvider'; import { FuelUIProvider, type FuelUIProviderProps } from './FuelUIProvider'; export { useFuel } from './FuelHooksProvider'; @@ -11,26 +13,36 @@ export { useConnectUI } from './FuelUIProvider'; type FuelProviderProps = { ui?: boolean; fuelConfig?: FuelConfig; + /** + * Whether enforce connectors to be on the desired the network. + * @default true + */ + chainId?: number; } & FuelUIProviderProps; export function FuelProvider({ - theme, + theme: _theme, children, fuelConfig, bridgeURL, ui = true, + chainId, }: FuelProviderProps) { + const theme = _theme || 'light'; if (ui) { return ( - - - {children} - + + + + {chainId != null && } + {children} + + ); } diff --git a/packages/react/src/providers/index.tsx b/packages/react/src/providers/index.tsx index 4e20f31d..60db477f 100644 --- a/packages/react/src/providers/index.tsx +++ b/packages/react/src/providers/index.tsx @@ -1 +1,2 @@ export * from './FuelProvider'; +export * from './FuelChainProvider'; diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 83336707..979396d9 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -15,4 +15,11 @@ export type Connector = { installed: boolean; }; +export type SvgIconProps = { + theme?: string; + className?: string; + onClick?: () => void; + size: number; +}; + export type ConnectorList = Array; diff --git a/packages/react/src/ui/Connect/components/Bridge/Bridge.tsx b/packages/react/src/ui/Connect/components/Bridge/Bridge.tsx index fe398264..1ab23440 100644 --- a/packages/react/src/ui/Connect/components/Bridge/Bridge.tsx +++ b/packages/react/src/ui/Connect/components/Bridge/Bridge.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { BRIDGE_URL } from '../../../../config'; +import { NoFundIcon } from '../../../../icons/NoFundIcon'; import { useConnectUI } from '../../../../providers/FuelUIProvider'; -import { NoFundIcon } from '../../icons/NoFundIcon'; import { ConnectorButton, ConnectorButtonPrimary, diff --git a/packages/react/src/ui/Connect/components/Connector/Connecting.tsx b/packages/react/src/ui/Connect/components/Connector/Connecting.tsx index 3b27e392..ac095dbe 100644 --- a/packages/react/src/ui/Connect/components/Connector/Connecting.tsx +++ b/packages/react/src/ui/Connect/components/Connector/Connecting.tsx @@ -3,7 +3,7 @@ import type { FuelConnector } from 'fuels'; import { useConnectUI } from '../../../../providers/FuelUIProvider'; import { ConnectorIcon } from '../ConnectorIcon'; -import { Spinner } from '../Spinner/Spinner'; +import { Spinner } from '../../../../icons/Spinner'; import { ConnectorButton, ConnectorButtonPrimary, diff --git a/packages/react/src/ui/Connect/components/ConnectorIcon.tsx b/packages/react/src/ui/Connect/components/ConnectorIcon.tsx index 513432f6..f20d7b7a 100644 --- a/packages/react/src/ui/Connect/components/ConnectorIcon.tsx +++ b/packages/react/src/ui/Connect/components/ConnectorIcon.tsx @@ -1,9 +1,9 @@ import type { ConnectorMetadata } from 'fuels'; -import type { SvgIconProps } from '../../types'; -import { FuelWalletDevelopmentIcon } from '../icons/FuelWalletDevelopmentIcon'; -import { FuelWalletIcon } from '../icons/FuelWalletIcon'; -import { FueletIcon } from '../icons/FueletIcon'; +import { FuelWalletDevelopmentIcon } from '../../../icons/FuelWalletDevelopmentIcon'; +import { FuelWalletIcon } from '../../../icons/FuelWalletIcon'; +import { FueletIcon } from '../../../icons/FueletIcon'; +import type { SvgIconProps } from '../../../types'; import { getImageUrl } from '../utils/getImageUrl'; type ConnectorIconProps = { diff --git a/packages/react/src/ui/Connect/index.tsx b/packages/react/src/ui/Connect/index.tsx index 5e0b7df1..0cd00715 100644 --- a/packages/react/src/ui/Connect/index.tsx +++ b/packages/react/src/ui/Connect/index.tsx @@ -8,17 +8,16 @@ import { Connectors } from './components/Connectors'; import { BackIcon, CloseIcon, - DialogContent, DialogMain, DialogOverlay, DialogTitle, Divider, FuelRoot, } from './styles'; -import { getThemeVariables } from './themes'; -import './index.css'; import type { FuelConnector } from 'fuels'; +import { getThemeVariables } from '../../constants/themes'; +import { DialogContent } from '../Dialog/components/Content'; import { Bridge } from './components/Bridge/Bridge'; import { Connecting } from './components/Connector/Connecting'; import { ExternalDisclaimer } from './components/ExternalDisclaimer/ExternalDisclaimer'; diff --git a/packages/react/src/ui/Connect/styles.tsx b/packages/react/src/ui/Connect/styles.tsx index fc9ab19e..62a36ebf 100644 --- a/packages/react/src/ui/Connect/styles.tsx +++ b/packages/react/src/ui/Connect/styles.tsx @@ -1,8 +1,8 @@ import * as Dialog from '@radix-ui/react-dialog'; import { keyframes, styled } from 'styled-components'; -import { BackIcon as CBackIcon } from './icons/BackIcon'; -import { CloseIcon as CCloseIcon } from './icons/CloseIcon'; +import { BackIcon as CBackIcon } from '../../icons/BackIcon'; +import { CloseIcon as CCloseIcon } from '../../icons/CloseIcon'; const overlayShow = keyframes` from { @@ -13,17 +13,6 @@ const overlayShow = keyframes` } `; -const contentShow = keyframes` - from { - opacity: 0; - transform: translate(-50%, -48%) scale(0.96); - } - to { - opacity: 1; - transform: translate(-50%, -50%) scale(1); - } -`; - const placeholderLoader = keyframes` 0%{ background-position: -468px 0 @@ -40,43 +29,6 @@ export const DialogOverlay = styled(Dialog.Overlay)` animation: ${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1); `; -export const DialogContent = styled(Dialog.Content)` - overflow: hidden; - color: var(--fuel-color); - user-select: none; - max-height: calc(100% - 20px); - box-sizing: border-box; - background-color: var(--fuel-dialog-background); - position: fixed; - left: 50%; - transform: translate(-50%, -50%); - border-radius: 36px; - padding: 14px 0px; - padding-bottom: 18px; - animation: ${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1); - box-shadow: - hsl(206 22% 7% / 35%) 0px 10px 38px -10px, - hsl(206 22% 7% / 20%) 0px 10px 20px -15px; - - &:focus { - outline: none; - } - - @media (min-width: 431px) { - top: 50%; - width: 360px; - max-width: calc(100% - 20px); - } - - @media (max-width: 430px) { - top: auto; - bottom: -246px; - width: 100vw; - max-width: 100%; - border-radius: 36px 36px 0 0; - } -` as unknown as typeof Dialog.Content; - export const DialogTitle = styled(Dialog.Title)` padding: 8px 14px 12px; margin: 0; diff --git a/packages/react/src/ui/Dialog/components/Content/index.tsx b/packages/react/src/ui/Dialog/components/Content/index.tsx new file mode 100644 index 00000000..ca39df3c --- /dev/null +++ b/packages/react/src/ui/Dialog/components/Content/index.tsx @@ -0,0 +1,45 @@ +import * as Dialog from '@radix-ui/react-dialog'; +import { keyframes, styled } from 'styled-components'; + +const contentShow = keyframes` + from { + opacity: 0; + transform: translate(-50%, -48%) scale(0.96); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +`; + +export const DialogContent = styled(Dialog.Content)` + overflow: hidden; + color: var(--fuel-color); + user-select: none; + max-height: calc(100% - 20px); + box-sizing: border-box; + background-color: var(--fuel-dialog-background); + position: fixed; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 36px; + padding: 14px 0px; + padding-bottom: 18px; + animation: ${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1); + top: 50%; + width: 360px; + max-width: calc(100% - 20px); + box-shadow: + hsl(206 22% 7% / 35%) 0px 10px 38px -10px, + hsl(206 22% 7% / 20%) 0px 10px 20px -15px; + + &:focus { + outline: none; + } + + @media (max-width: 430px) { + top: 50%; + width: 100%; + border-radius: 36px; + } +` as unknown as typeof Dialog.Content; diff --git a/packages/react/src/ui/NetworkMonitor/components/NetworkSwitchDialog/index.tsx b/packages/react/src/ui/NetworkMonitor/components/NetworkSwitchDialog/index.tsx new file mode 100644 index 00000000..a77e3730 --- /dev/null +++ b/packages/react/src/ui/NetworkMonitor/components/NetworkSwitchDialog/index.tsx @@ -0,0 +1,103 @@ +import type { FuelConnector } from 'fuels'; +import { useMemo } from 'react'; +import { NATIVE_CONNECTORS } from '../../../../config'; +import { useSelectNetwork } from '../../../../hooks/useSelectNetwork'; +import { Spinner } from '../../../../icons/Spinner'; +import { useFuelChain } from '../../../../providers'; +import { + Button, + ButtonDisconnect, + ButtonLoading, + Container, + Description, + Divider, + ErrorMessage, + Header, + OrLabel, + Title, +} from './styles'; + +export function NetworkSwitchDialog({ + currentConnector, + close, +}: { currentConnector: FuelConnector | undefined | null; close: () => void }) { + const { chainId } = useFuelChain(); + const { selectNetwork, isError, error, isPending } = useSelectNetwork(); + const canSwitch = useMemo( + () => + currentConnector?.name && + NATIVE_CONNECTORS.includes(currentConnector?.name), + [currentConnector], + ); + + if (!currentConnector) { + return null; + } + + function getErrorMessage() { + if (isError) { + return error?.message || 'Failed to switch network'; + } + if (!canSwitch) { + return 'This connector does not support switching networks.'; + } + return ''; + } + + const description = `This app does not support the current connected network.${ + canSwitch ? ' Switch or disconnect to continue.' : '' + }`; + + function handleSwitch() { + chainId != null && selectNetwork({ chainId }, { onSuccess: close }); + } + + function handleDisconnect() { + currentConnector?.disconnect(); + close(); + } + + return ( + +
+ Network Switch Required + {description} + {(!!isError || !canSwitch) && ( + {getErrorMessage()} + )} +
+ {!isPending && ( +