diff --git a/examples/react/src/components/Connected.tsx b/examples/react/src/components/Connected.tsx index 3b2c800b1..8ecfde5a0 100644 --- a/examples/react/src/components/Connected.tsx +++ b/examples/react/src/components/Connected.tsx @@ -507,7 +507,11 @@ export const Connected = () => { Demos - setOpenWalletModal(true)} /> + setOpenWalletModal(true)} + /> {(sponsoredContractAddresses[chainId] || networkForCurrentChainId.testnet) && isWaasConnectionActive && ( void closeAddFunds: () => void addFundsSettings?: AddFundsSettings diff --git a/packages/checkout/src/contexts/SwapModal.ts b/packages/checkout/src/contexts/SwapModal.ts index 1666ed34a..ea3fbe63e 100644 --- a/packages/checkout/src/contexts/SwapModal.ts +++ b/packages/checkout/src/contexts/SwapModal.ts @@ -24,6 +24,7 @@ export interface SwapModalSettings { } type SwapModalContext = { + isSwapModalOpen: boolean openSwapModal: (settings: SwapModalSettings) => void closeSwapModal: () => void swapModalSettings?: SwapModalSettings diff --git a/packages/checkout/src/hooks/useAddFundsModal.ts b/packages/checkout/src/hooks/useAddFundsModal.ts index ae4db7bcf..cf09cc840 100644 --- a/packages/checkout/src/hooks/useAddFundsModal.ts +++ b/packages/checkout/src/hooks/useAddFundsModal.ts @@ -8,6 +8,7 @@ import { AddFundsSettings, useAddFundsModalContext } from '../contexts/AddFundsM * @property Current settings for the On-ramp modal `addFundsSettings` */ type UseAddFundsModalReturnType = { + isAddFundsModalOpen: boolean triggerAddFunds: (settings: AddFundsSettings) => void closeAddFunds: () => void addFundsSettings: AddFundsSettings | undefined @@ -52,7 +53,7 @@ type UseAddFundsModalReturnType = { * ``` */ export const useAddFundsModal = (): UseAddFundsModalReturnType => { - const { triggerAddFunds, closeAddFunds, addFundsSettings } = useAddFundsModalContext() + const { isAddFundsModalOpen, triggerAddFunds, closeAddFunds, addFundsSettings } = useAddFundsModalContext() - return { triggerAddFunds, closeAddFunds, addFundsSettings } + return { isAddFundsModalOpen, triggerAddFunds, closeAddFunds, addFundsSettings } } diff --git a/packages/checkout/src/hooks/useSwapModal.ts b/packages/checkout/src/hooks/useSwapModal.ts index 5f4d3964b..a2db176ef 100644 --- a/packages/checkout/src/hooks/useSwapModal.ts +++ b/packages/checkout/src/hooks/useSwapModal.ts @@ -8,6 +8,7 @@ import { SwapModalSettings, useSwapModalContext } from '../contexts/SwapModal' * @property {SwapModalSettings|undefined} swapModalSettings - Current settings for the Swap modal */ type UseSwapModalReturnType = { + isSwapModalOpen: boolean openSwapModal: (settings: SwapModalSettings) => void closeSwapModal: () => void swapModalSettings: SwapModalSettings | undefined @@ -73,7 +74,7 @@ type UseSwapModalReturnType = { * ``` */ export const useSwapModal = (): UseSwapModalReturnType => { - const { openSwapModal, closeSwapModal, swapModalSettings } = useSwapModalContext() + const { isSwapModalOpen, openSwapModal, closeSwapModal, swapModalSettings } = useSwapModalContext() - return { openSwapModal, closeSwapModal, swapModalSettings } + return { isSwapModalOpen, openSwapModal, closeSwapModal, swapModalSettings } } diff --git a/packages/connect/src/components/SequenceConnectPreviewProvider/SequenceConnectPreviewProvider.tsx b/packages/connect/src/components/SequenceConnectPreviewProvider/SequenceConnectPreviewProvider.tsx index b2d91a30d..501625839 100644 --- a/packages/connect/src/components/SequenceConnectPreviewProvider/SequenceConnectPreviewProvider.tsx +++ b/packages/connect/src/components/SequenceConnectPreviewProvider/SequenceConnectPreviewProvider.tsx @@ -119,7 +119,9 @@ export const SequenceConnectPreviewProvider = (props: SequenceConnectProviderPro }} > - +
diff --git a/packages/connect/src/components/SequenceConnectProvider/SequenceConnectProvider.tsx b/packages/connect/src/components/SequenceConnectProvider/SequenceConnectProvider.tsx index ca702b7aa..72a19d766 100644 --- a/packages/connect/src/components/SequenceConnectProvider/SequenceConnectProvider.tsx +++ b/packages/connect/src/components/SequenceConnectProvider/SequenceConnectProvider.tsx @@ -165,7 +165,9 @@ export const SequenceConnectProvider = (props: SequenceConnectProviderProps) => }} > - + diff --git a/packages/connect/src/contexts/ConnectModal.ts b/packages/connect/src/contexts/ConnectModal.ts index 6d48c2764..3dc017a14 100644 --- a/packages/connect/src/contexts/ConnectModal.ts +++ b/packages/connect/src/contexts/ConnectModal.ts @@ -5,6 +5,7 @@ import React from 'react' import { createGenericContext } from './genericContext' type ConnectModalContext = { + isConnectModalOpen: boolean setOpenConnectModal: React.Dispatch> openConnectModalState: boolean } diff --git a/packages/connect/src/hooks/useOpenConnectModal.ts b/packages/connect/src/hooks/useOpenConnectModal.ts index fedb4fe87..b1c0d210c 100644 --- a/packages/connect/src/hooks/useOpenConnectModal.ts +++ b/packages/connect/src/hooks/useOpenConnectModal.ts @@ -7,6 +7,7 @@ import { useConnectModalContext } from '../contexts/ConnectModal' * @property Current open state of the Connect modal `openConnectModalState` */ type UseOpenConnectModalReturnType = { + isConnectModalOpen: boolean setOpenConnectModal: (isOpen: boolean) => void openConnectModalState: boolean } @@ -47,7 +48,7 @@ type UseOpenConnectModalReturnType = { * ``` */ export const useOpenConnectModal = (): UseOpenConnectModalReturnType => { - const { setOpenConnectModal, openConnectModalState } = useConnectModalContext() + const { isConnectModalOpen, setOpenConnectModal, openConnectModalState } = useConnectModalContext() - return { setOpenConnectModal, openConnectModalState } + return { isConnectModalOpen, setOpenConnectModal, openConnectModalState } } diff --git a/packages/connect/src/hooks/useWallets.ts b/packages/connect/src/hooks/useWallets.ts index f23736e5a..323e19a73 100644 --- a/packages/connect/src/hooks/useWallets.ts +++ b/packages/connect/src/hooks/useWallets.ts @@ -159,6 +159,7 @@ export interface ConnectedWallet { address: string isActive: boolean isEmbedded: boolean + signInMethod: string } /** @@ -222,6 +223,7 @@ export interface UseWalletsReturnType { * } * ``` */ + export const useWallets = (): UseWalletsReturnType => { const { address } = useAccount() const connections = useConnections() @@ -261,12 +263,21 @@ export const useWallets = (): UseWalletsReturnType => { name: getConnectorName(connection.connector), address: connection.accounts[0], isActive: connection.accounts[0] === address, - isEmbedded: connection.connector.id.includes('waas') + isEmbedded: connection.connector.id.includes('waas'), + signInMethod: (connection.connector._wallet as any)?.id })) const setActiveWallet = async (walletAddress: string) => { - const connection = connections.find((c: UseConnectionsReturnType[number]) => c.accounts[0] === walletAddress) + const connection = connections.find( + (c: UseConnectionsReturnType[number]) => c.accounts[0].toLowerCase() === walletAddress.toLowerCase() + ) if (!connection) { + console.error('No connection found for wallet address:', walletAddress) + return + } + + // Do not try to change if it's already active + if (wallets.find(w => w.address.toLowerCase() === walletAddress.toLowerCase())?.isActive) { return } @@ -278,7 +289,9 @@ export const useWallets = (): UseWalletsReturnType => { } const disconnectWallet = async (walletAddress: string) => { - const connection = connections.find((c: UseConnectionsReturnType[number]) => c.accounts[0] === walletAddress) + const connection = connections.find( + (c: UseConnectionsReturnType[number]) => c.accounts[0].toLowerCase() === walletAddress.toLowerCase() + ) if (!connection) { return } diff --git a/packages/connect/src/styles.ts b/packages/connect/src/styles.ts index 395c4da54..b8541913c 100644 --- a/packages/connect/src/styles.ts +++ b/packages/connect/src/styles.ts @@ -6,6 +6,7 @@ export const styles = String.raw` --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-mono: "Roboto", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --color-violet-600: oklch(0.541 0.281 293.009); --color-black: #000; --color-white: #fff; --spacing: 0.25rem; @@ -198,9 +199,6 @@ export const styles = String.raw` } } @layer utilities { - .pointer-events-auto { - pointer-events: auto; - } .pointer-events-none { pointer-events: none; } @@ -285,6 +283,9 @@ export const styles = String.raw` .-m-\[1px\] { margin: calc(1px * -1); } + .m-0 { + margin: calc(var(--spacing) * 0); + } .m-4 { margin: calc(var(--spacing) * 4); } @@ -306,6 +307,9 @@ export const styles = String.raw` .my-4 { margin-block: calc(var(--spacing) * 4); } + .mt-0 { + margin-top: calc(var(--spacing) * 0); + } .mt-0\.5 { margin-top: calc(var(--spacing) * 0.5); } @@ -327,9 +331,6 @@ export const styles = String.raw` .mt-6 { margin-top: calc(var(--spacing) * 6); } - .mt-8 { - margin-top: calc(var(--spacing) * 8); - } .mt-10 { margin-top: calc(var(--spacing) * 10); } @@ -363,9 +364,6 @@ export const styles = String.raw` .ml-2 { margin-left: calc(var(--spacing) * 2); } - .ml-\[-16px\] { - margin-left: -16px; - } .block { display: block; } @@ -387,6 +385,9 @@ export const styles = String.raw` .inline-flex { display: inline-flex; } + .table { + display: table; + } .aspect-square { aspect-ratio: 1 / 1; } @@ -435,9 +436,6 @@ export const styles = String.raw` .h-\[1px\] { height: 1px; } - .h-\[14px\] { - height: 14px; - } .h-\[52px\] { height: 52px; } @@ -480,6 +478,9 @@ export const styles = String.raw` .min-h-full { min-height: 100%; } + .w-1 { + width: calc(var(--spacing) * 1); + } .w-1\/2 { width: calc(1/2 * 100%); } @@ -528,9 +529,6 @@ export const styles = String.raw` .w-\[1px\] { width: 1px; } - .w-\[14px\] { - width: 14px; - } .w-\[52px\] { width: 52px; } @@ -555,9 +553,6 @@ export const styles = String.raw` .w-screen { width: 100vw; } - .max-w-1\/2 { - max-width: calc(1/2 * 100%); - } .max-w-\[532px\] { max-width: 532px; } @@ -579,12 +574,21 @@ export const styles = String.raw` .min-w-full { min-width: 100%; } + .flex-shrink { + flex-shrink: 1; + } .shrink-0 { flex-shrink: 0; } + .flex-grow { + flex-grow: 1; + } .grow { flex-grow: 1; } + .border-collapse { + border-collapse: collapse; + } .origin-top { transform-origin: top; } @@ -700,6 +704,9 @@ export const styles = String.raw` .overflow-scroll { overflow: scroll; } + .overflow-visible { + overflow: visible; + } .overflow-x-auto { overflow-x: auto; } @@ -715,6 +722,9 @@ export const styles = String.raw` .overscroll-y-contain { overscroll-behavior-y: contain; } + .rounded { + border-radius: 0.25rem; + } .rounded-full { border-radius: calc(infinity * 1px); } @@ -724,6 +734,9 @@ export const styles = String.raw` .rounded-md { border-radius: var(--radius-md); } + .rounded-none { + border-radius: 0; + } .rounded-sm { border-radius: var(--radius-sm); } @@ -737,10 +750,22 @@ export const styles = String.raw` border-top-left-radius: var(--radius-2xl); border-top-right-radius: var(--radius-2xl); } + .rounded-t-none { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + .rounded-t-xl { + border-top-left-radius: var(--radius-xl); + border-top-right-radius: var(--radius-xl); + } .rounded-b-none { border-bottom-right-radius: 0; border-bottom-left-radius: 0; } + .rounded-b-xl { + border-bottom-right-radius: var(--radius-xl); + border-bottom-left-radius: var(--radius-xl); + } .border { border-style: var(--tw-border-style); border-width: 1px; @@ -782,6 +807,9 @@ export const styles = String.raw` .border-transparent { border-color: transparent; } + .border-violet-600 { + border-color: var(--color-violet-600); + } .border-b-primary { border-bottom-color: var(--seq-color-primary); } @@ -881,6 +909,9 @@ export const styles = String.raw` .p-5 { padding: calc(var(--spacing) * 5); } + .p-6 { + padding: calc(var(--spacing) * 6); + } .p-\[10px\] { padding: 10px; } @@ -926,6 +957,9 @@ export const styles = String.raw` .pt-0 { padding-top: calc(var(--spacing) * 0); } + .pt-1 { + padding-top: calc(var(--spacing) * 1); + } .pt-1\.5 { padding-top: calc(var(--spacing) * 1.5); } @@ -941,6 +975,9 @@ export const styles = String.raw` .pt-5 { padding-top: calc(var(--spacing) * 5); } + .pt-6 { + padding-top: calc(var(--spacing) * 6); + } .pt-\[60px\] { padding-top: 60px; } @@ -977,6 +1014,9 @@ export const styles = String.raw` .pl-2 { padding-left: calc(var(--spacing) * 2); } + .pl-3 { + padding-left: calc(var(--spacing) * 3); + } .pl-4 { padding-left: calc(var(--spacing) * 4); } @@ -986,12 +1026,6 @@ export const styles = String.raw` .text-center { text-align: center; } - .text-left { - text-align: left; - } - .text-right { - text-align: right; - } .font-body { font-family: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; } @@ -1211,6 +1245,9 @@ export const styles = String.raw` .ring-border-normal { --tw-ring-color: var(--seq-color-border-normal); } + .ring-white { + --tw-ring-color: var(--color-white); + } .ring-white\/10 { --tw-ring-color: color-mix(in oklab, var(--color-white) 10%, transparent); } @@ -1246,6 +1283,10 @@ export const styles = String.raw` -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); } + .backdrop-filter { + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } .transition { transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); diff --git a/packages/hooks/src/hooks/Combination/useGetSwapQuote.ts b/packages/hooks/src/hooks/Combination/useGetSwapQuote.ts index 473ef5245..2ede5d883 100644 --- a/packages/hooks/src/hooks/Combination/useGetSwapQuote.ts +++ b/packages/hooks/src/hooks/Combination/useGetSwapQuote.ts @@ -106,6 +106,12 @@ export const useGetSwapQuote = (getSwapQuoteArgs: GetSwapQuoteV2Args, options?: retry: options?.retry ?? true, staleTime: time.oneMinute * 1, enabled: - !options?.disabled || !getSwapQuoteArgs.userAddress || !getSwapQuoteArgs.chainId || !getSwapQuoteArgs.buyCurrencyAddress + !options?.disabled && + !!getSwapQuoteArgs.userAddress && + !!getSwapQuoteArgs.buyCurrencyAddress && + !!getSwapQuoteArgs.sellCurrencyAddress && + getSwapQuoteArgs.buyAmount !== '0' && + !!getSwapQuoteArgs.chainId && + !!getSwapQuoteArgs.includeApprove }) } diff --git a/packages/hooks/src/hooks/Indexer/useGetTransactionHistorySummary.ts b/packages/hooks/src/hooks/Indexer/useGetTransactionHistorySummary.ts index efc5be77f..66a805cad 100644 --- a/packages/hooks/src/hooks/Indexer/useGetTransactionHistorySummary.ts +++ b/packages/hooks/src/hooks/Indexer/useGetTransactionHistorySummary.ts @@ -7,20 +7,19 @@ import { HooksOptions } from '../../types' import { useIndexerClients } from './useIndexerClient' -export interface GetTransactionHistorySummaryArgs { - accountAddress: string - chainIds: number[] -} +export type GetTransactionHistorySummaryArgs = + | { accountAddress: string; accountAddresses?: never; chainIds: number[] } + | { accountAddress?: never; accountAddresses: string[]; chainIds: number[] } const getTransactionHistorySummary = async ( indexerClients: Map, - { accountAddress }: GetTransactionHistorySummaryArgs + { accountAddress, accountAddresses }: GetTransactionHistorySummaryArgs ): Promise => { const histories = await Promise.all( Array.from(indexerClients.values()).map(indexerClient => indexerClient.getTransactionHistory({ filter: { - accountAddress + accountAddresses: accountAddresses || [accountAddress] }, includeMetadata: true }) @@ -130,7 +129,7 @@ export const useGetTransactionHistorySummary = ( refetchOnMount: true, enabled: getTransactionHistorySummaryArgs.chainIds.length > 0 && - !!getTransactionHistorySummaryArgs.accountAddress && + !!(getTransactionHistorySummaryArgs.accountAddress || (getTransactionHistorySummaryArgs.accountAddresses?.length ?? 0)) && !options?.disabled }) } diff --git a/packages/wallet-widget/package.json b/packages/wallet-widget/package.json index fe029f5e2..0f2f62c3d 100644 --- a/packages/wallet-widget/package.json +++ b/packages/wallet-widget/package.json @@ -33,6 +33,7 @@ "@0xsequence/design-system": "2.1.6", "@0xsequence/hooks": "workspace:*", "@radix-ui/react-popover": "^1.0.7", + "micro-observables": "1.7.2", "dayjs": "^1.11.11", "fuse.js": "^6.6.2", "qrcode.react": "^4.0.1", @@ -45,6 +46,8 @@ "@0xsequence/metadata": ">=2.3.7", "@0xsequence/network": ">=2.3.7", "@0xsequence/connect": "workspace:*", + "@0xsequence/hooks": "workspace:*", + "@0xsequence/checkout": "workspace:*", "@tanstack/react-query": ">= 5", "ethers": ">= 6.13.0", "react": ">= 17", @@ -54,8 +57,11 @@ }, "devDependencies": { "@0xsequence/connect": "workspace:*", + "@0xsequence/hooks": "workspace:*", + "@0xsequence/checkout": "workspace:*", "@tanstack/react-query": "^5.62.0", "@types/react-copy-to-clipboard": "^5.0.7", + "@yudiel/react-qr-scanner": "2.2.1", "ethers": "^6.13.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/packages/wallet-widget/src/components/Alert.tsx b/packages/wallet-widget/src/components/Alert.tsx index b3660b2d2..7e24d4c17 100644 --- a/packages/wallet-widget/src/components/Alert.tsx +++ b/packages/wallet-widget/src/components/Alert.tsx @@ -19,7 +19,7 @@ const variants = { export const Alert = ({ title, description, secondaryDescription, variant, buttonProps, children }: AlertProps) => { return (
-
+
diff --git a/packages/wallet-widget/src/components/CoinRow.tsx b/packages/wallet-widget/src/components/CoinRow.tsx deleted file mode 100644 index 7c0861767..000000000 --- a/packages/wallet-widget/src/components/CoinRow.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { formatDisplay } from '@0xsequence/connect' -import { Skeleton, Text, TokenImage } from '@0xsequence/design-system' -import React from 'react' -import { formatUnits } from 'viem' - -import { getPercentageColor } from '../utils' - -interface CoinRowProps { - name: string - symbol: string - decimals: number - balance: string - imageUrl?: string - fiatValue: string - priceChangePercentage: number -} - -export const CoinRowSkeleton = () => { - return ( -
-
- -
- - -
-
-
- - -
-
- ) -} - -export const CoinRow = ({ imageUrl, name, decimals, balance, symbol, fiatValue, priceChangePercentage }: CoinRowProps) => { - const formattedBalance = formatUnits(BigInt(balance), decimals) - const balanceDisplayed = formatDisplay(formattedBalance) - - return ( -
-
- -
- - {name} - - - {' '} - {`${balanceDisplayed} ${symbol}`} - -
-
-
- {`$${fiatValue}`} - - {priceChangePercentage.toFixed(2)}% - -
-
- ) -} diff --git a/packages/wallet-widget/src/components/ConnectorLogos/AppleLogo.tsx b/packages/wallet-widget/src/components/ConnectorLogos/AppleLogo.tsx new file mode 100644 index 000000000..565bec975 --- /dev/null +++ b/packages/wallet-widget/src/components/ConnectorLogos/AppleLogo.tsx @@ -0,0 +1,20 @@ +interface AppleLogoProps { + isDarkMode: boolean +} + +export const AppleLogo: React.FC = ({ isDarkMode }) => { + const fillColor = isDarkMode ? 'white' : 'black' + + return ( + + + + + ) +} diff --git a/packages/wallet-widget/src/components/ConnectorLogos/CoinbaseWalletLogo.tsx b/packages/wallet-widget/src/components/ConnectorLogos/CoinbaseWalletLogo.tsx new file mode 100644 index 000000000..6600e21dc --- /dev/null +++ b/packages/wallet-widget/src/components/ConnectorLogos/CoinbaseWalletLogo.tsx @@ -0,0 +1,13 @@ +export const CoinbaseWalletLogo: React.FC = () => { + return ( + + + + + ) +} diff --git a/packages/wallet-widget/src/components/ConnectorLogos/DiscordLogo.tsx b/packages/wallet-widget/src/components/ConnectorLogos/DiscordLogo.tsx new file mode 100644 index 000000000..aa6fefcf3 --- /dev/null +++ b/packages/wallet-widget/src/components/ConnectorLogos/DiscordLogo.tsx @@ -0,0 +1,24 @@ +interface DiscordLogoProps { + isDarkMode: boolean +} + +export const DiscordLogo: React.FC = ({ isDarkMode }) => { + const fillColor = isDarkMode ? 'white' : 'black' + + return ( + + + + + + + + + + + ) +} diff --git a/packages/wallet-widget/src/components/ConnectorLogos/EmailLogo.tsx b/packages/wallet-widget/src/components/ConnectorLogos/EmailLogo.tsx new file mode 100644 index 000000000..210038d05 --- /dev/null +++ b/packages/wallet-widget/src/components/ConnectorLogos/EmailLogo.tsx @@ -0,0 +1,20 @@ +interface EmailLogoProps { + isDarkMode: boolean +} + +export const EmailLogo: React.FC = ({ isDarkMode }) => { + const fillColor = isDarkMode ? 'white' : 'black' + + return ( + + + + + ) +} diff --git a/packages/wallet-widget/src/components/ConnectorLogos/FacebookLogo.tsx b/packages/wallet-widget/src/components/ConnectorLogos/FacebookLogo.tsx new file mode 100644 index 000000000..56448c82e --- /dev/null +++ b/packages/wallet-widget/src/components/ConnectorLogos/FacebookLogo.tsx @@ -0,0 +1,27 @@ +export const FacebookLogo: React.FC = () => { + return ( + + + + + + + + + + + ) +} diff --git a/packages/wallet-widget/src/components/ConnectorLogos/GoogleLogo.tsx b/packages/wallet-widget/src/components/ConnectorLogos/GoogleLogo.tsx new file mode 100644 index 000000000..cd6d52603 --- /dev/null +++ b/packages/wallet-widget/src/components/ConnectorLogos/GoogleLogo.tsx @@ -0,0 +1,30 @@ +export const GoogleLogo: React.FC = () => { + return ( + + + + + + + + + + + ) +} diff --git a/packages/wallet-widget/src/components/ConnectorLogos/MetaMaskLogo.tsx b/packages/wallet-widget/src/components/ConnectorLogos/MetaMaskLogo.tsx new file mode 100644 index 000000000..bf20ddff8 --- /dev/null +++ b/packages/wallet-widget/src/components/ConnectorLogos/MetaMaskLogo.tsx @@ -0,0 +1,52 @@ +export const MetaMaskLogo: React.FC = () => ( + + + + + + + + + + + + + + +) diff --git a/packages/wallet-widget/src/components/ConnectorLogos/SequenceLogo.tsx b/packages/wallet-widget/src/components/ConnectorLogos/SequenceLogo.tsx new file mode 100644 index 000000000..b0784e284 --- /dev/null +++ b/packages/wallet-widget/src/components/ConnectorLogos/SequenceLogo.tsx @@ -0,0 +1,134 @@ +export const SequenceLogo: React.FC = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/packages/wallet-widget/src/components/ConnectorLogos/TwitchLogo.tsx b/packages/wallet-widget/src/components/ConnectorLogos/TwitchLogo.tsx new file mode 100644 index 000000000..18ea6519e --- /dev/null +++ b/packages/wallet-widget/src/components/ConnectorLogos/TwitchLogo.tsx @@ -0,0 +1,31 @@ +interface TwitchLogoProps { + isDarkMode: boolean +} + +export const TwitchLogo: React.FC = ({ isDarkMode }) => { + const fillColor = isDarkMode ? 'white' : 'black' + + return ( + + + + + + + + + + ) +} diff --git a/packages/wallet-widget/src/components/ConnectorLogos/WalletConnectLogo.tsx b/packages/wallet-widget/src/components/ConnectorLogos/WalletConnectLogo.tsx new file mode 100644 index 000000000..38aa0f477 --- /dev/null +++ b/packages/wallet-widget/src/components/ConnectorLogos/WalletConnectLogo.tsx @@ -0,0 +1,10 @@ +export const WalletConnectLogo: React.FC = () => { + return ( + + + + ) +} diff --git a/packages/wallet-widget/src/components/ConnectorLogos/getConnectorLogos.tsx b/packages/wallet-widget/src/components/ConnectorLogos/getConnectorLogos.tsx new file mode 100644 index 000000000..374f25ae0 --- /dev/null +++ b/packages/wallet-widget/src/components/ConnectorLogos/getConnectorLogos.tsx @@ -0,0 +1,45 @@ +import { ReactNode } from 'react' + +import { AppleLogo } from './AppleLogo' +import { CoinbaseWalletLogo } from './CoinbaseWalletLogo' +import { DiscordLogo } from './DiscordLogo' +import { EmailLogo } from './EmailLogo' +import { FacebookLogo } from './FacebookLogo' +import { GoogleLogo } from './GoogleLogo' +import { MetaMaskLogo } from './MetaMaskLogo' +import { SequenceLogo } from './SequenceLogo' +import { TwitchLogo } from './TwitchLogo' +import { WalletConnectLogo } from './WalletConnectLogo' + +export const getConnectorLogo = (connectorId: string, isDarkMode = false): ReactNode => { + switch (connectorId) { + case 'apple-waas': + return + case 'email-waas': + return + case 'google-waas': + return + case 'apple': + return + case 'coinbase-wallet': + return + case 'discord': + return + case 'email': + return + case 'facebook': + return + case 'google': + return + case 'metamask-wallet': + return + case 'sequence': + return + case 'twitch': + return + case 'wallet-connect': + return + default: + return <> + } +} diff --git a/packages/wallet-widget/src/components/CopyButton.tsx b/packages/wallet-widget/src/components/CopyButton.tsx index bf8f3dd7b..fa306a99d 100644 --- a/packages/wallet-widget/src/components/CopyButton.tsx +++ b/packages/wallet-widget/src/components/CopyButton.tsx @@ -17,7 +17,7 @@ export const CopyButton = (props: CopyButtonProps) => { if (isCopied) { setTimeout(() => { setCopy(false) - }, 4000) + }, 2000) } }, [isCopied]) diff --git a/packages/wallet-widget/src/components/DefaultIcon.tsx b/packages/wallet-widget/src/components/DefaultIcon.tsx deleted file mode 100644 index 4698c6650..000000000 --- a/packages/wallet-widget/src/components/DefaultIcon.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Text } from '@0xsequence/design-system' -import React from 'react' - -interface DefaultIconProps { - size?: number -} - -export const DefaultIcon = ({ size = 30 }: DefaultIconProps) => { - return ( -
- - ? - -
- ) -} diff --git a/packages/wallet-widget/src/components/Filter/CollectionsFilter.tsx b/packages/wallet-widget/src/components/Filter/CollectionsFilter.tsx new file mode 100644 index 000000000..6420c1f61 --- /dev/null +++ b/packages/wallet-widget/src/components/Filter/CollectionsFilter.tsx @@ -0,0 +1,76 @@ +import { TokenImage, Text } from '@0xsequence/design-system' +import { useGetTokenBalancesSummary } from '@0xsequence/hooks' +import { ContractType } from '@0xsequence/indexer' + +import { useSettings } from '../../hooks' +import { MediaIconWrapper } from '../IconWrappers' +import { ListCardSelect } from '../ListCard/ListCardSelect' + +export const CollectionsFilter = () => { + const { selectedCollectionsObservable, selectedNetworks, selectedWallets, setSelectedCollections } = useSettings() + const selectedCollections = selectedCollectionsObservable.get() + + const { data: tokens } = useGetTokenBalancesSummary({ + chainIds: selectedNetworks, + filter: { + accountAddresses: selectedWallets.map(wallet => wallet.address), + omitNativeBalances: true + } + }) + + const collections = tokens + ?.filter(token => token.contractType === ContractType.ERC721 || token.contractType === ContractType.ERC1155) + .map(collection => { + return { + contractAddress: collection.contractAddress, + chainId: collection.chainId, + contractInfo: { + name: collection.contractInfo?.name || '', + logoURI: collection.contractInfo?.logoURI || '' + } + } + }) + + return ( +
+ {collections?.length && collections.length > 1 && ( + setSelectedCollections([])} + > + ( +
+ +
+ ))} + size="sm" + /> + + All + +
+ )} + {collections?.map(collection => ( + c.contractAddress === collection.contractAddress && c.chainId === collection.chainId) || + collections.length === 1 + } + onClick={collections.length > 1 ? () => setSelectedCollections([collection]) : undefined} + > + + + {collection.contractInfo?.name} + + + ))} +
+ ) +} diff --git a/packages/wallet-widget/src/components/Filter/FilterButton.tsx b/packages/wallet-widget/src/components/Filter/FilterButton.tsx new file mode 100644 index 000000000..21f248939 --- /dev/null +++ b/packages/wallet-widget/src/components/Filter/FilterButton.tsx @@ -0,0 +1,43 @@ +import { useWallets } from '@0xsequence/connect' +import { FilterIcon, cn, cardVariants, Text } from '@0xsequence/design-system' +import { AnimatePresence } from 'motion/react' +import { useMemo, useState } from 'react' + +import { useSettings } from '../../hooks' + +import { FilterMenu } from './FilterMenu' + +export const FilterButton = ({ label, type }: { label: string; type: 'tokens' | 'collectibles' | 'transactions' }) => { + const { wallets } = useWallets() + const { selectedWallets, selectedNetworks, selectedCollections, allNetworks } = useSettings() + const [isOpen, setIsOpen] = useState(false) + + const howManyModifiedFilters = useMemo(() => { + const isModifiedWallets = Number(selectedWallets.length !== wallets.length) + const isModifiedNetworks = Number(selectedNetworks.length !== allNetworks.length) + const isModifiedCollections = Number(selectedCollections.length !== 0) + + return isModifiedWallets + isModifiedNetworks + isModifiedCollections + }, [selectedWallets, wallets, selectedNetworks, allNetworks, selectedCollections]) + + return ( +
setIsOpen(true)} + > + +
+ {howManyModifiedFilters > 0 && ( +
+ + {howManyModifiedFilters} + +
+ )} +
+ + {isOpen && setIsOpen(false)} label={label} type={type} />} +
+ ) +} diff --git a/packages/wallet-widget/src/components/Filter/FilterMenu.tsx b/packages/wallet-widget/src/components/Filter/FilterMenu.tsx new file mode 100644 index 000000000..29f16d644 --- /dev/null +++ b/packages/wallet-widget/src/components/Filter/FilterMenu.tsx @@ -0,0 +1,193 @@ +import { formatAddress, getNetwork, useWallets } from '@0xsequence/connect' +import { Text, TokenImage } from '@0xsequence/design-system' +import { useGetTokenBalancesSummary } from '@0xsequence/hooks' +import { ContractType } from '@0xsequence/indexer' +import { useObservable } from 'micro-observables' +import { useState } from 'react' + +import { useSettings } from '../../hooks' +import { StackedIconTag } from '../IconWrappers' +import { ListCardNav } from '../ListCard' +import { SlideupDrawer } from '../Select/SlideupDrawer' + +import { CollectionsFilter } from './CollectionsFilter' +import { NetworkImageCustom } from './NetworkImageCustom' +import { NetworksFilter } from './NetworksFilter' +import { WalletsFilter } from './WalletsFilter' + +enum FilterType { + menu = 'Filters', + wallets = 'Select active Wallets', + networks = 'Select active Networks', + collections = 'Select active Collections' +} + +export const FilterMenu = ({ + label, + type, + onClose +}: { + label: string + type: 'tokens' | 'collectibles' | 'transactions' + onClose: () => void +}) => { + const { wallets } = useWallets() + const { selectedWalletsObservable, selectedNetworksObservable, selectedCollectionsObservable } = useSettings() + const selectedWallets = useObservable(selectedWalletsObservable) + const selectedNetworks = useObservable(selectedNetworksObservable) + const selectedCollections = useObservable(selectedCollectionsObservable) + + const [selectedFilter, setSelectedFilter] = useState(FilterType.menu) + + const { data: tokens } = useGetTokenBalancesSummary({ + chainIds: selectedNetworks, + filter: { + accountAddresses: selectedWallets.map(wallet => wallet.address), + omitNativeBalances: true + } + }) + + const collections = tokens + ?.filter(token => token.contractType === ContractType.ERC721 || token.contractType === ContractType.ERC1155) + .map(collection => { + return { + contractAddress: collection.contractAddress, + contractInfo: { + name: collection.contractInfo?.name || '', + logoURI: collection.contractInfo?.logoURI || '' + } + } + }) + + const walletsPreview = + selectedWallets.length > 1 || wallets.length === 1 ? ( + + All +
+ } + /> + ) : ( +
+ + {formatAddress(selectedWallets[0].address)} + + } + /> +
+ ) + + const networksPreview = + selectedNetworks.length > 1 ? ( +
+ + All + + } + /> +
+ ) : ( + ]} + label={ + + {getNetwork(selectedNetworks[0]).title} + + } + /> + ) + + const collectionsPreview = + collections?.length === 0 ? ( + + N/A + + ) : selectedCollections.length === 0 ? ( + + All + + } + /> + ) : ( + + ]} + label={ + + {selectedCollections[0].contractInfo?.name} + + } + /> + ) + + const handleFilterChange = (filter: FilterType) => { + setSelectedFilter(filter) + } + + return ( + handleFilterChange(FilterType.menu) : undefined} + > + {selectedFilter === FilterType.menu ? ( +
+ handleFilterChange(FilterType.wallets)} + > + + Wallets + + + handleFilterChange(FilterType.networks)} + > + + Networks + + + {type === 'collectibles' && ( + handleFilterChange(FilterType.collections)} + > + + Collections + + + )} +
+ ) : selectedFilter === FilterType.wallets ? ( + + ) : selectedFilter === FilterType.networks ? ( + + ) : selectedFilter === FilterType.collections ? ( + + ) : null} +
+ ) +} diff --git a/packages/wallet-widget/src/components/Filter/NetworkImageCustom.tsx b/packages/wallet-widget/src/components/Filter/NetworkImageCustom.tsx new file mode 100644 index 000000000..271192655 --- /dev/null +++ b/packages/wallet-widget/src/components/Filter/NetworkImageCustom.tsx @@ -0,0 +1,37 @@ +import { getNetwork } from '@0xsequence/connect' +import { NetworkImage } from '@0xsequence/design-system' + +export const NetworkImageCustom = ({ + chainId, + indicatorPosition = 'top-left', + style, + className +}: { + chainId: number + indicatorPosition?: 'top-left' | 'top-right' + style?: React.CSSProperties + className?: string +}) => { + const network = getNetwork(chainId) + const isTestnet = network.testnet + return ( +
+ {isTestnet && ( +
+ )} + + +
+ ) +} diff --git a/packages/wallet-widget/src/components/Filter/NetworkRow.tsx b/packages/wallet-widget/src/components/Filter/NetworkRow.tsx new file mode 100644 index 000000000..6a807a115 --- /dev/null +++ b/packages/wallet-widget/src/components/Filter/NetworkRow.tsx @@ -0,0 +1,21 @@ +import { getNetwork } from '@0xsequence/connect' +import { Text } from '@0xsequence/design-system' + +import { ListCardSelect } from '../ListCard' + +import { NetworkImageCustom } from './NetworkImageCustom' + +export const NetworkRow = ({ chainId, isSelected, onClick }: { chainId: number; isSelected: boolean; onClick: () => void }) => { + const network = getNetwork(chainId) + const title = network.title + return ( + +
+ + + {title} + +
+
+ ) +} diff --git a/packages/wallet-widget/src/components/Filter/NetworksFilter.tsx b/packages/wallet-widget/src/components/Filter/NetworksFilter.tsx new file mode 100644 index 000000000..e09867045 --- /dev/null +++ b/packages/wallet-widget/src/components/Filter/NetworksFilter.tsx @@ -0,0 +1,41 @@ +import { Text } from '@0xsequence/design-system' +import { useObservable } from 'micro-observables' + +import { useSettings } from '../../hooks' +import { MediaIconWrapper } from '../IconWrappers' +import { ListCardSelect } from '../ListCard/ListCardSelect' + +import { NetworkImageCustom } from './NetworkImageCustom' +import { NetworkRow } from './NetworkRow' + +export const NetworksFilter = () => { + const { selectedNetworksObservable, setSelectedNetworks, allNetworks } = useSettings() + const selectedNetworks = useObservable(selectedNetworksObservable) + // NetworksFilter is using an observable since its used in settings detached from FilterMenu + + return ( +
+ {allNetworks.length > 1 && ( + 1} onClick={() => setSelectedNetworks([])}> + ( + + ))} + size="sm" + /> + + All + + + )} + {allNetworks.map(chainId => ( + setSelectedNetworks([chainId])} + /> + ))} +
+ ) +} diff --git a/packages/wallet-widget/src/components/Filter/TokenImageCustom.tsx b/packages/wallet-widget/src/components/Filter/TokenImageCustom.tsx new file mode 100644 index 000000000..f1c070249 --- /dev/null +++ b/packages/wallet-widget/src/components/Filter/TokenImageCustom.tsx @@ -0,0 +1,42 @@ +import { TokenImage } from '@0xsequence/design-system' + +import { NetworkImageCustom } from './NetworkImageCustom' + +const NETWORK_IMAGE_SIZE = '45%' +const NETWORK_IMAGE_OFFSET = '-1px' + +export const TokenImageCustom = ({ + src, + symbol, + chainId +}: { + src: string | undefined + symbol: string | undefined + chainId: number +}) => { + return ( +
+
+ +
+ + +
+ ) +} diff --git a/packages/wallet-widget/src/components/Filter/WalletsFilter.tsx b/packages/wallet-widget/src/components/Filter/WalletsFilter.tsx new file mode 100644 index 000000000..aae1afde0 --- /dev/null +++ b/packages/wallet-widget/src/components/Filter/WalletsFilter.tsx @@ -0,0 +1,60 @@ +import { formatAddress, useWallets } from '@0xsequence/connect' +import { Text } from '@0xsequence/design-system' + +import { useSettings } from '../../hooks' +import { useFiatWalletsMap } from '../../hooks/useFiatWalletsMap' +import { MediaIconWrapper } from '../IconWrappers' +import { ListCardSelect } from '../ListCard/ListCardSelect' +import { WalletAccountGradient } from '../WalletAccountGradient' + +export const WalletsFilter = () => { + const { selectedWalletsObservable, setSelectedWallets, fiatCurrency } = useSettings() + const { fiatWalletsMap } = useFiatWalletsMap() + const { wallets } = useWallets() + + const totalFiatValue = fiatWalletsMap.reduce((acc, wallet) => acc + Number(wallet.fiatValue), 0).toFixed(2) + + return ( +
+ {wallets.length > 1 && ( + 1} + rightChildren={ + + {fiatCurrency.sign} + {totalFiatValue} + + } + onClick={() => setSelectedWallets([])} + > + wallet.address)} size="sm" isAccount /> + + All + + + )} + {wallets.map(wallet => ( + w.address === wallet.address) !== undefined + } + rightChildren={ + + {fiatCurrency.sign} + {fiatWalletsMap.find(w => w.accountAddress === wallet.address)?.fiatValue} + + } + onClick={() => setSelectedWallets([wallet])} + > + + + {formatAddress(wallet.address)} + + + ))} +
+ ) +} diff --git a/packages/wallet-widget/src/components/IconWrappers/MediaIconWrapper.tsx b/packages/wallet-widget/src/components/IconWrappers/MediaIconWrapper.tsx new file mode 100644 index 000000000..8c9749004 --- /dev/null +++ b/packages/wallet-widget/src/components/IconWrappers/MediaIconWrapper.tsx @@ -0,0 +1,109 @@ +import { GradientAvatar } from '@0xsequence/design-system' + +const widthClassMap = { + '4xs': '16px', + '2xs': '24px', + sm: '32px', + base: '44px', + lg: '56px', + '2lg': '64px', + '3lg': '80px' +} + +export const MediaIconWrapper = ({ + iconList, + isAccount = false, + size = 'base', + shape = 'rounded' +}: { + iconList: string[] | React.ReactNode[] + isAccount?: boolean + size?: '4xs' | '2xs' | 'sm' | 'base' | 'lg' | '2lg' | '3lg' + shape?: 'rounded' | 'square' +}) => { + const firstThreeIcons = iconList.slice(0, 3) + + let partialWidth = 0 + let shapeClass = 'rounded-lg' + + switch (size) { + case '4xs': + partialWidth = 8 + shapeClass = 'rounded-sm' + break + case '2xs': + partialWidth = 12 + shapeClass = 'rounded-md' + break + case 'sm': + partialWidth = 16 + break + case 'base': + partialWidth = 22 + break + case 'lg': + partialWidth = 28 + break + case '2lg': + partialWidth = 32 + break + case '3lg': + partialWidth = 40 + break + } + + const width = firstThreeIcons.length * partialWidth + partialWidth + + if (shape === 'rounded') { + shapeClass = 'rounded-full' + } + + return ( +
+ {firstThreeIcons.map((icon, index) => ( +
+ {typeof icon === 'string' ? ( + <> + {isAccount ? ( +
+ +
+ ) : ( +
+ icon +
+ )} + + ) : ( +
+ {icon} +
+ )} +
+ ))} +
+ ) +} diff --git a/packages/wallet-widget/src/components/IconWrappers/StackedIconTag.tsx b/packages/wallet-widget/src/components/IconWrappers/StackedIconTag.tsx new file mode 100644 index 000000000..0c1b7dbf0 --- /dev/null +++ b/packages/wallet-widget/src/components/IconWrappers/StackedIconTag.tsx @@ -0,0 +1,29 @@ +import { MediaIconWrapper } from './MediaIconWrapper' + +export const StackedIconTag = ({ + iconList, + isAccount = false, + shape = 'rounded', + label = undefined, + onClick, + enabled = false +}: { + iconList: string[] | React.ReactNode[] + isAccount?: boolean + shape?: 'rounded' | 'square' + label?: React.ReactNode + onClick?: () => void + enabled?: boolean +}) => { + const shapeClass = shape === 'rounded' ? 'rounded-full' : 'rounded-lg' + return ( +
+ {iconList.length > 0 && } + {label} +
+ ) +} diff --git a/packages/wallet-widget/src/components/IconWrappers/index.ts b/packages/wallet-widget/src/components/IconWrappers/index.ts new file mode 100644 index 000000000..af3eae319 --- /dev/null +++ b/packages/wallet-widget/src/components/IconWrappers/index.ts @@ -0,0 +1,2 @@ +export { StackedIconTag } from './StackedIconTag' +export { MediaIconWrapper } from './MediaIconWrapper' diff --git a/packages/wallet-widget/src/components/InfiniteScroll.tsx b/packages/wallet-widget/src/components/InfiniteScroll.tsx index 00368c17e..20588c3b9 100644 --- a/packages/wallet-widget/src/components/InfiniteScroll.tsx +++ b/packages/wallet-widget/src/components/InfiniteScroll.tsx @@ -22,16 +22,21 @@ export const useIntersectionObserver = (ref: RefObject, options? interface InfiniteScrollProps { onLoad: (pageNumber: number) => Promise hasMore?: boolean + resetTrigger?: boolean } export const InfiniteScroll = (props: PropsWithChildren) => { - const { onLoad, hasMore = true, children } = props + const { onLoad, hasMore = true, children, resetTrigger } = props const [pageNumber, setPageNumber] = useState(0) const [isLoading, setLoading] = useState(false) const bottomRef = useRef(null) const isBottom = useIntersectionObserver(bottomRef) + useEffect(() => { + setPageNumber(0) + }, [resetTrigger]) + useEffect(() => { if (isBottom && hasMore && !isLoading) { handleLoad() diff --git a/packages/wallet-widget/src/components/ListCard/ListCardNav.tsx b/packages/wallet-widget/src/components/ListCard/ListCardNav.tsx new file mode 100644 index 000000000..24032ace4 --- /dev/null +++ b/packages/wallet-widget/src/components/ListCard/ListCardNav.tsx @@ -0,0 +1,39 @@ +import { cn, ChevronRightIcon } from '@0xsequence/design-system' + +export const ListCardNav = ({ + children, + rightChildren, + shape = 'rounded', + style, + type = 'chevron', + onClick, + disabled = false +}: { + children: React.ReactNode + rightChildren?: React.ReactNode + shape?: 'rounded' | 'square' + style?: React.CSSProperties + type?: 'chevron' | 'custom' + onClick: () => void + disabled?: boolean +}) => { + return ( +
+
{children}
+ + {(rightChildren || type === 'chevron') && ( +
+ {rightChildren} + {type === 'chevron' && } +
+ )} +
+ ) +} diff --git a/packages/wallet-widget/src/components/ListCard/ListCardSelect.tsx b/packages/wallet-widget/src/components/ListCard/ListCardSelect.tsx new file mode 100644 index 000000000..4bb6844ac --- /dev/null +++ b/packages/wallet-widget/src/components/ListCard/ListCardSelect.tsx @@ -0,0 +1,43 @@ +import { cn } from '@0xsequence/design-system' + +import { RadioSelector } from './RadioSelector' + +export const ListCardSelect = ({ + children, + rightChildren, + shape = 'rounded', + style, + type = 'radio', + isSelected = false, + disabled = false, + onClick +}: { + children: React.ReactNode + rightChildren?: React.ReactNode + shape?: 'rounded' | 'square' + style?: React.CSSProperties + type?: 'radio' | 'custom' + isSelected?: boolean + disabled?: boolean + onClick?: () => void +}) => { + return ( +
+
{children}
+ +
+ {rightChildren} + {type === 'radio' && } +
+
+ ) +} diff --git a/packages/wallet-widget/src/components/ListCard/RadioSelector.tsx b/packages/wallet-widget/src/components/ListCard/RadioSelector.tsx new file mode 100644 index 000000000..8e71201f4 --- /dev/null +++ b/packages/wallet-widget/src/components/ListCard/RadioSelector.tsx @@ -0,0 +1,28 @@ +import { CheckmarkIcon } from '@0xsequence/design-system' +import { motion } from 'motion/react' + +interface RadioSelectorProps { + isSelected: boolean + className?: string +} + +export const RadioSelector = (props: RadioSelectorProps) => { + const { isSelected, className } = props + return ( +
+ + + +
+ ) +} diff --git a/packages/wallet-widget/src/components/ListCard/index.ts b/packages/wallet-widget/src/components/ListCard/index.ts new file mode 100644 index 000000000..178617a88 --- /dev/null +++ b/packages/wallet-widget/src/components/ListCard/index.ts @@ -0,0 +1,2 @@ +export { ListCardNav } from './ListCardNav' +export { ListCardSelect } from './ListCardSelect' diff --git a/packages/wallet-widget/src/components/ListCardTable/ListCardNavTable.tsx b/packages/wallet-widget/src/components/ListCardTable/ListCardNavTable.tsx new file mode 100644 index 000000000..17e762aff --- /dev/null +++ b/packages/wallet-widget/src/components/ListCardTable/ListCardNavTable.tsx @@ -0,0 +1,28 @@ +import { Card, Divider } from '@0xsequence/design-system' + +export const ListCardNavTable = ({ + children, + navItems, + style +}: { + children: React.ReactNode + navItems: React.ReactNode[] + style?: React.CSSProperties +}) => { + return ( +
+ + {children} + + {navItems.map((navItem, index) => ( +
+ + {navItem} +
+ ))} +
+ ) +} diff --git a/packages/wallet-widget/src/components/ListCardTable/ListCardSelectTable.tsx b/packages/wallet-widget/src/components/ListCardTable/ListCardSelectTable.tsx new file mode 100644 index 000000000..495c05b08 --- /dev/null +++ b/packages/wallet-widget/src/components/ListCardTable/ListCardSelectTable.tsx @@ -0,0 +1 @@ +// TODO: Implement ListCardSelectTable diff --git a/packages/wallet-widget/src/components/ListCardTable/index.ts b/packages/wallet-widget/src/components/ListCardTable/index.ts new file mode 100644 index 000000000..2ec0adf8e --- /dev/null +++ b/packages/wallet-widget/src/components/ListCardTable/index.ts @@ -0,0 +1,3 @@ +export { ListCardNavTable } from './ListCardNavTable' +// export { ListCardSelectTable } from './ListCardSelectTable' +// TODO: uncomment this when ListCardSelectTable is implemented diff --git a/packages/wallet-widget/src/components/Loader.tsx b/packages/wallet-widget/src/components/Loader.tsx deleted file mode 100644 index 037f82dd2..000000000 --- a/packages/wallet-widget/src/components/Loader.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Spinner } from '@0xsequence/design-system' -import React from 'react' - -export const Loader = () => { - return ( -
-
- -
-
- ) -} diff --git a/packages/wallet-widget/src/components/NavigationHeader/index.tsx b/packages/wallet-widget/src/components/NavigationHeader/index.tsx index 219b57215..7431c823f 100644 --- a/packages/wallet-widget/src/components/NavigationHeader/index.tsx +++ b/packages/wallet-widget/src/components/NavigationHeader/index.tsx @@ -22,10 +22,9 @@ export const NavigationHeader = ({ secondaryText, primaryText }: NavigationHeade return (
{history.length > 0 ? ( @@ -44,14 +43,14 @@ export const NavigationHeader = ({ secondaryText, primaryText }: NavigationHeade {secondaryText} - + {primaryText}
diff --git a/packages/wallet-widget/src/components/OptionsFooter.tsx b/packages/wallet-widget/src/components/OptionsFooter.tsx new file mode 100644 index 000000000..548218bfb --- /dev/null +++ b/packages/wallet-widget/src/components/OptionsFooter.tsx @@ -0,0 +1,48 @@ +import { cardVariants, cn, Text } from '@0xsequence/design-system' + +export const OptionsFooter = ({ + primaryButtonText, + onPrimaryButtonClick, + secondaryButtonText, + onSecondaryButtonClick, + shape = 'round' +}: { + primaryButtonText: string + onPrimaryButtonClick: () => void + secondaryButtonText?: string + onSecondaryButtonClick?: () => void + shape?: 'round' | 'square' +}) => { + return ( +
+ {secondaryButtonText && ( +
+ + {secondaryButtonText} + +
+ )} + +
+ + {primaryButtonText} + +
+
+ ) +} diff --git a/packages/wallet-widget/src/components/SearchLists/CollectiblesList.tsx b/packages/wallet-widget/src/components/SearchLists/CollectiblesList.tsx new file mode 100644 index 000000000..94fb552a3 --- /dev/null +++ b/packages/wallet-widget/src/components/SearchLists/CollectiblesList.tsx @@ -0,0 +1,118 @@ +import { SearchIcon, TextInput } from '@0xsequence/design-system' +import { TokenBalance } from '@0xsequence/indexer' +import Fuse from 'fuse.js' +import { useState, useMemo } from 'react' + +import { TokenBalanceWithPrice, useGetMoreBalances } from '../../utils' +import { FilterButton } from '../Filter/FilterButton' + +import { CollectiblesTab } from './CollectiblesList/CollectiblesTab' + +export const CollectiblesList = ({ + tokenBalancesData, + isPendingTokenBalances, + onTokenClick, + enableFilters = true, + gridColumns = 2 +}: { + tokenBalancesData: TokenBalance[] + isPendingTokenBalances: boolean + onTokenClick: (token: TokenBalanceWithPrice) => void + enableFilters?: boolean + gridColumns?: number +}) => { + const pageSize = 8 + + const [search, setSearch] = useState('') + + const collectibleBalancesUnordered = + tokenBalancesData?.filter(b => b.contractType === 'ERC721' || b.contractType === 'ERC1155') || [] + + const collectibleBalances = collectibleBalancesUnordered.sort((a, b) => { + return Number(b.balance) - Number(a.balance) + }) + + const collectibleBalancesWithPrice = collectibleBalances.map(balance => ({ + ...balance, + price: { + value: 0, + currency: 'USD' + } + })) + + const isPending = isPendingTokenBalances + + const fuseOptions = { + threshold: 0.1, + ignoreLocation: true, + keys: [ + { + name: 'name', + getFn: (token: TokenBalance) => { + return token.tokenMetadata?.name || '' + } + }, + { + name: 'collectionName', + getFn: (token: TokenBalance) => { + return token.contractInfo?.name || '' + } + } + ] + } + + const fuse = useMemo(() => { + return new Fuse(collectibleBalancesWithPrice, fuseOptions) + }, [collectibleBalancesWithPrice]) + + const searchResults = useMemo(() => { + if (!search.trimStart()) { + return [] + } + return fuse.search(search).map(result => result.item) + }, [search, fuse]) + + const { + data: infiniteBalances, + fetchNextPage: fetchMoreBalances, + hasNextPage: hasMoreBalances, + isFetching: isFetchingMoreBalances + } = useGetMoreBalances(collectibleBalancesWithPrice, pageSize, { enabled: search.trim() === '' }) + + const { + data: infiniteSearchBalances, + fetchNextPage: fetchMoreSearchBalances, + hasNextPage: hasMoreSearchBalances, + isFetching: isFetchingMoreSearchBalances + } = useGetMoreBalances(searchResults, pageSize, { enabled: search.trim() !== '' }) + + return ( +
+
+
+ setSearch(ev.target.value)} + placeholder="Search your wallet" + data-1p-ignore + /> +
+ {enableFilters && } +
+
+ +
+
+ ) +} diff --git a/packages/wallet-widget/src/components/SearchLists/CollectiblesList/CollectibleTile.tsx b/packages/wallet-widget/src/components/SearchLists/CollectiblesList/CollectibleTile.tsx new file mode 100644 index 000000000..4a1755838 --- /dev/null +++ b/packages/wallet-widget/src/components/SearchLists/CollectiblesList/CollectibleTile.tsx @@ -0,0 +1,43 @@ +import { NetworkImage } from '@0xsequence/design-system' +import { useGetTokenMetadata } from '@0xsequence/hooks' + +import { TokenBalanceWithPrice } from '../../../utils' +import { CollectibleTileImage } from '../../CollectibleTileImage' + +const NETWORK_IMAGE_SIZE = '15%' +const NETWORK_IMAGE_OFFSET = '2%' + +interface CollectibleTileProps { + balance: TokenBalanceWithPrice + onTokenClick: (token: TokenBalanceWithPrice) => void +} + +export const CollectibleTile = ({ balance, onTokenClick }: CollectibleTileProps) => { + const onClick = () => { + onTokenClick(balance) + } + + const { data: tokenMetadata } = useGetTokenMetadata({ + chainID: String(balance.chainId), + contractAddress: balance.contractAddress, + tokenIDs: [balance.tokenID || ''] + }) + + const imageUrl = tokenMetadata?.[0]?.image + + return ( +
+ + +
+ ) +} diff --git a/packages/wallet-widget/src/components/SearchLists/CollectiblesList/CollectiblesTab.tsx b/packages/wallet-widget/src/components/SearchLists/CollectiblesList/CollectiblesTab.tsx new file mode 100644 index 000000000..4b332c29b --- /dev/null +++ b/packages/wallet-widget/src/components/SearchLists/CollectiblesList/CollectiblesTab.tsx @@ -0,0 +1,57 @@ +import { Spinner, Skeleton, Text } from '@0xsequence/design-system' +import React from 'react' + +import { TokenBalanceWithPrice } from '../../../utils' +import { InfiniteScroll } from '../../InfiniteScroll' + +import { CollectibleTile } from './CollectibleTile' + +interface CollectiblesTabProps { + displayedCollectibleBalances: TokenBalanceWithPrice[] | undefined + fetchMoreCollectibleBalances: () => Promise + hasMoreCollectibleBalances: boolean + isFetchingMoreCollectibleBalances: boolean + isFetchingInitialBalances: boolean + onTokenClick: (token: TokenBalanceWithPrice) => void + gridColumns?: number +} + +export const CollectiblesTab: React.FC = ({ + displayedCollectibleBalances, + fetchMoreCollectibleBalances, + hasMoreCollectibleBalances, + isFetchingMoreCollectibleBalances, + isFetchingInitialBalances, + onTokenClick, + gridColumns +}) => { + return ( +
+
+ {isFetchingInitialBalances ? ( + <> + {Array(6) + .fill(null) + .map((_, i) => ( + + ))} + + ) : ( + <> + {displayedCollectibleBalances && displayedCollectibleBalances.length > 0 && ( + fetchMoreCollectibleBalances()} hasMore={hasMoreCollectibleBalances}> + {displayedCollectibleBalances?.map((balance, index) => { + return + })} + + )} + + )} +
+ {(!displayedCollectibleBalances || displayedCollectibleBalances.length === 0) && !isFetchingMoreCollectibleBalances && ( + No Collectibles Found + )} + {isFetchingMoreCollectibleBalances && } +
+ ) +} diff --git a/packages/wallet-widget/src/components/SearchLists/TokenList.tsx b/packages/wallet-widget/src/components/SearchLists/TokenList.tsx new file mode 100644 index 000000000..93f9724dd --- /dev/null +++ b/packages/wallet-widget/src/components/SearchLists/TokenList.tsx @@ -0,0 +1,158 @@ +import { compareAddress, getNativeTokenInfoByChainId } from '@0xsequence/connect' +import { SearchIcon, TextInput } from '@0xsequence/design-system' +import { useGetCoinPrices, useGetExchangeRate } from '@0xsequence/hooks' +import { TokenBalance } from '@0xsequence/indexer' +import { ethers } from 'ethers' +import Fuse from 'fuse.js' +import { useState, useMemo } from 'react' +import { useConfig } from 'wagmi' + +import { useSettings } from '../../hooks' +import { computeBalanceFiat, TokenBalanceWithPrice } from '../../utils' +import { useGetMoreBalances } from '../../utils' +import { FilterButton } from '../Filter/FilterButton' + +import { CoinsTab } from './TokenList/CoinsTab' + +export const TokenList = ({ + tokenBalancesData, + isPendingTokenBalances, + onTokenClick, + includeUserAddress = false, + enableFilters = true +}: { + tokenBalancesData: TokenBalance[] + isPendingTokenBalances: boolean + onTokenClick: (token: TokenBalanceWithPrice) => void + enableFilters?: boolean + includeUserAddress?: boolean +}) => { + const pageSize = 10 + + const { chains } = useConfig() + const { fiatCurrency } = useSettings() + + const [search, setSearch] = useState('') + + const coinBalancesUnordered = + tokenBalancesData?.filter(b => b.contractType === 'ERC20' || compareAddress(b.contractAddress, ethers.ZeroAddress)) || [] + + const { data: coinPrices = [], isPending: isPendingCoinPrices } = useGetCoinPrices( + coinBalancesUnordered.map(token => ({ + chainId: token.chainId, + contractAddress: token.contractAddress + })) + ) + + const { data: conversionRate = 1, isPending: isPendingConversionRate } = useGetExchangeRate(fiatCurrency.symbol) + + const coinBalances = coinBalancesUnordered.sort((a, b) => { + const fiatA = computeBalanceFiat({ + balance: a, + prices: coinPrices, + conversionRate, + decimals: a.contractInfo?.decimals || 18 + }) + const fiatB = computeBalanceFiat({ + balance: b, + prices: coinPrices, + conversionRate, + decimals: b.contractInfo?.decimals || 18 + }) + return Number(fiatB) - Number(fiatA) + }) + + const coinBalancesWithPrices = coinBalances.map(balance => { + const matchingPrice = coinPrices.find(price => { + const isSameChainAndAddress = + price.token.chainId === balance.chainId && price.token.contractAddress === balance.contractAddress + + const isTokenIdMatch = + price.token.tokenId === balance.tokenID || !(balance.contractType === 'ERC721' || balance.contractType === 'ERC1155') + + return isSameChainAndAddress && isTokenIdMatch + }) + + const priceValue = (matchingPrice?.price?.value || 0) * conversionRate + const priceCurrency = fiatCurrency.symbol + + return { + ...balance, + price: { value: priceValue, currency: priceCurrency } + } + }) + + const isPending = isPendingTokenBalances || isPendingCoinPrices || isPendingConversionRate + + const fuseOptions = { + threshold: 0.1, + ignoreLocation: true, + keys: [ + { + name: 'name', + getFn: (token: TokenBalance) => { + if (compareAddress(token.contractAddress, ethers.ZeroAddress)) { + const nativeTokenInfo = getNativeTokenInfoByChainId(token.chainId, chains) + return nativeTokenInfo.name + } + return token.contractInfo?.name || 'Unknown' + } + } + ] + } + + const fuse = useMemo(() => { + return new Fuse(coinBalancesWithPrices, fuseOptions) + }, [coinBalancesWithPrices]) + + const searchResults = useMemo(() => { + if (!search.trimStart()) { + return [] + } + return fuse.search(search).map(result => result.item) + }, [search, fuse]) + + const { + data: infiniteBalances, + fetchNextPage: fetchMoreBalances, + hasNextPage: hasMoreBalances, + isFetching: isFetchingMoreBalances + } = useGetMoreBalances(coinBalancesWithPrices, pageSize, { enabled: search.trim() === '' }) + + const { + data: infiniteSearchBalances, + fetchNextPage: fetchMoreSearchBalances, + hasNextPage: hasMoreSearchBalances, + isFetching: isFetchingMoreSearchBalances + } = useGetMoreBalances(searchResults, pageSize, { enabled: search.trim() !== '' }) + + return ( +
+
+
+ setSearch(ev.target.value)} + placeholder="Search your wallet" + data-1p-ignore + /> +
+ {enableFilters && } +
+
+ +
+
+ ) +} diff --git a/packages/wallet-widget/src/components/SearchLists/TokenList/CoinRow.tsx b/packages/wallet-widget/src/components/SearchLists/TokenList/CoinRow.tsx new file mode 100644 index 000000000..a27a27dea --- /dev/null +++ b/packages/wallet-widget/src/components/SearchLists/TokenList/CoinRow.tsx @@ -0,0 +1,65 @@ +import { formatAddress } from '@0xsequence/connect' +import { Text, GradientAvatar } from '@0xsequence/design-system' +import { getAddress } from 'viem' +import { useChains } from 'wagmi' + +import { useSettings } from '../../../hooks' +import { formatTokenInfo } from '../../../utils/formatBalance' +import { TokenBalanceWithPrice } from '../../../utils/tokens' +import { TokenImageCustom } from '../../Filter/TokenImageCustom' +import { ListCardNav } from '../../ListCard/ListCardNav' + +interface BalanceItemProps { + balance: TokenBalanceWithPrice + includeUserAddress?: boolean + onTokenClick: (token: TokenBalanceWithPrice) => void +} + +export const CoinRow = ({ balance, onTokenClick, includeUserAddress = false }: BalanceItemProps) => { + const { fiatCurrency } = useSettings() + const chains = useChains() + const { logo, name, symbol, displayBalance, fiatBalance } = formatTokenInfo(balance, fiatCurrency.sign, chains) + + const userAddress = getAddress(balance.accountAddress) + + const onClick = () => { + onTokenClick(balance) + } + + return ( + +
+ +
+
+
+ + {name} + +
+ {includeUserAddress && ( +
+ + + {formatAddress(userAddress)} + +
+ )} +
+
+
+
+
+ + {displayBalance} + +
+ + {fiatBalance} + +
+
+
+
+ ) +} diff --git a/packages/wallet-widget/src/components/SearchLists/TokenList/CoinsTab.tsx b/packages/wallet-widget/src/components/SearchLists/TokenList/CoinsTab.tsx new file mode 100644 index 000000000..1a1e3bb50 --- /dev/null +++ b/packages/wallet-widget/src/components/SearchLists/TokenList/CoinsTab.tsx @@ -0,0 +1,58 @@ +import { Spinner, Skeleton, Text } from '@0xsequence/design-system' +import React from 'react' + +import { TokenBalanceWithPrice } from '../../../utils/tokens' +import { InfiniteScroll } from '../../InfiniteScroll' + +import { CoinRow } from './CoinRow' + +interface CoinsTabProps { + displayedCoinBalances: TokenBalanceWithPrice[] | undefined + fetchMoreCoinBalances: () => Promise + hasMoreCoinBalances: boolean + isFetchingMoreCoinBalances: boolean + isFetchingInitialBalances: boolean + onTokenClick: (token: TokenBalanceWithPrice) => void + includeUserAddress?: boolean +} + +export const CoinsTab: React.FC = ({ + displayedCoinBalances, + fetchMoreCoinBalances, + hasMoreCoinBalances, + isFetchingMoreCoinBalances, + isFetchingInitialBalances, + onTokenClick, + includeUserAddress = false +}) => { + return ( +
+
+ {isFetchingInitialBalances ? ( + <> + {Array(7) + .fill(null) + .map((_, i) => ( + + ))} + + ) : ( + <> + {(!displayedCoinBalances || displayedCoinBalances.length === 0) && !isFetchingMoreCoinBalances ? ( + No Coins Found + ) : ( + fetchMoreCoinBalances()} hasMore={hasMoreCoinBalances}> + {displayedCoinBalances?.map((balance, index) => { + return ( + + ) + })} + + )} + + )} + {isFetchingMoreCoinBalances && } +
+
+ ) +} diff --git a/packages/wallet-widget/src/components/SearchLists/index.ts b/packages/wallet-widget/src/components/SearchLists/index.ts new file mode 100644 index 000000000..212f44627 --- /dev/null +++ b/packages/wallet-widget/src/components/SearchLists/index.ts @@ -0,0 +1,2 @@ +export * from './TokenList' +export * from './CollectiblesList' diff --git a/packages/wallet-widget/src/components/Select/NetworkSelect.tsx b/packages/wallet-widget/src/components/Select/NetworkSelect.tsx new file mode 100644 index 000000000..6c9162342 --- /dev/null +++ b/packages/wallet-widget/src/components/Select/NetworkSelect.tsx @@ -0,0 +1,58 @@ +import { cardVariants, ChevronUpDownIcon, cn, NetworkImage, Text } from '@0xsequence/design-system' +import { useState } from 'react' +import { useChainId, useChains, useSwitchChain } from 'wagmi' + +import { NetworkRow } from '../Filter/NetworkRow' + +import { SlideupDrawer } from './SlideupDrawer' + +const NETWORK_SELECT_HEIGHT = '70px' + +export const NetworkSelect = () => { + const chains = useChains() + const chainId = useChainId() + const { switchChain } = useSwitchChain() + const [isOpen, setIsOpen] = useState(false) + + return ( +
setIsOpen(true)} + > +
+ + Network + +
+ + + {chains.find(chain => chain.id === chainId)?.name || chainId} + +
+
+ + + {isOpen && ( + setIsOpen(false)}> +
+ {chains.map(chain => ( + { + switchChain({ chainId: chain.id }) + setIsOpen(false) + }} + /> + ))} +
+
+ )} +
+ ) +} diff --git a/packages/wallet-widget/src/components/Select/SelectWalletRow.tsx b/packages/wallet-widget/src/components/Select/SelectWalletRow.tsx new file mode 100644 index 000000000..b55755b52 --- /dev/null +++ b/packages/wallet-widget/src/components/Select/SelectWalletRow.tsx @@ -0,0 +1,51 @@ +import { formatAddress, ConnectedWallet } from '@0xsequence/connect' +import { Text } from '@0xsequence/design-system' + +import { useSettings } from '../../hooks' +import { useFiatWalletsMap } from '../../hooks/useFiatWalletsMap' +import { CopyButton } from '../CopyButton' +import { ListCardSelect } from '../ListCard/ListCardSelect' +import { WalletAccountGradient } from '../WalletAccountGradient' + +export const SelectWalletRow = ({ + wallet, + isSelected = false, + onClick, + onClose +}: { + wallet: ConnectedWallet + isSelected?: boolean + onClick: (address: string) => void + onClose: () => void +}) => { + const { fiatCurrency } = useSettings() + const { fiatWalletsMap } = useFiatWalletsMap() + + function onSelectWallet() { + onClick(wallet.address) + onClose() + } + + const fiatValue = fiatWalletsMap.find(w => w.accountAddress === wallet.address)?.fiatValue + + return ( + + {fiatCurrency.sign} + {fiatValue} + + } + onClick={onSelectWallet} + isSelected={wallet.isActive || isSelected} + > + +
+ + {formatAddress(wallet.address)} + + +
+
+ ) +} diff --git a/packages/wallet-widget/src/components/Select/SlideupDrawer.tsx b/packages/wallet-widget/src/components/Select/SlideupDrawer.tsx new file mode 100644 index 000000000..c3b067f99 --- /dev/null +++ b/packages/wallet-widget/src/components/Select/SlideupDrawer.tsx @@ -0,0 +1,138 @@ +import { cardVariants, cn, Divider, Text, ChevronLeftIcon, Button } from '@0xsequence/design-system' +import { motion } from 'motion/react' +import { useContext, useEffect, useState } from 'react' +import ReactDOM from 'react-dom' + +import { WALLET_WIDTH } from '../../constants' +import { WalletContentRefContext } from '../../contexts/WalletContentRef' + +export const SlideupDrawer = ({ + label, + children, + buttonLabel, + dragHandleWidth = 64, + onClose, + handleButtonPress, + onBackPress +}: { + label: string + children: React.ReactNode + buttonLabel?: string + dragHandleWidth?: number + onClose: () => void + handleButtonPress?: () => void + onBackPress?: () => void +}) => { + const [walletContentHeight, setWalletContentHeight] = useState(0) + const walletContentRef = useContext(WalletContentRefContext) + + useEffect(() => { + const rect = walletContentRef?.current?.getBoundingClientRect() + if (rect) { + setWalletContentHeight(rect.height) + } + }, [walletContentRef]) + + if (!walletContentRef.current) { + return null + } + + return ReactDOM.createPortal( + <> + { + e.stopPropagation() + onClose() + }} + /> + { + if (info.offset.y > 100) { + onClose() + } + }} + style={{ + maxWidth: WALLET_WIDTH, + position: 'fixed', + bottom: 0, + left: 0, + width: '100%', + display: 'flex', + flexDirection: 'column', + zIndex: 30 + }} + onClick={e => e.stopPropagation()} + > +
+
+ {onBackPress && ( + + )} +
+
+
+
+
+ + {label} + +
+
+ {onBackPress &&
} +
+
+ {children} +
+ {buttonLabel && ( + <> + +
+
+ + {buttonLabel} + +
+
+ + )} +
+ + , + walletContentRef.current + ) +} diff --git a/packages/wallet-widget/src/components/Select/WalletSelect.tsx b/packages/wallet-widget/src/components/Select/WalletSelect.tsx new file mode 100644 index 000000000..876769989 --- /dev/null +++ b/packages/wallet-widget/src/components/Select/WalletSelect.tsx @@ -0,0 +1,53 @@ +import { useWallets } from '@0xsequence/connect' +import { ChevronUpDownIcon, Text } from '@0xsequence/design-system' +import { useState } from 'react' + +import { SelectWalletRow } from './SelectWalletRow' +import { SlideupDrawer } from './SlideupDrawer' + +const WALLET_SELECT_HEIGHT = '60px' + +export const WalletSelect = ({ selectedWallet, onClick }: { selectedWallet: string; onClick: (address: string) => void }) => { + const { wallets } = useWallets() + const [isOpen, setIsOpen] = useState(false) + + const activeWallet = wallets.find(wallet => wallet.isActive) + + const allButActiveWallet = wallets.filter(wallet => wallet.address !== activeWallet?.address) + + const handleClick = (address: string) => { + onClick(address) + setIsOpen(false) + } + + return ( +
setIsOpen(true)} + > +
+ + Select Connected Wallet + +
+ + + {isOpen && ( + setIsOpen(false)}> +
+ {allButActiveWallet.map(wallet => ( + setIsOpen(false)} + /> + ))} +
+
+ )} +
+ ) +} diff --git a/packages/wallet-widget/src/components/SelectButton/SelectButton.tsx b/packages/wallet-widget/src/components/SelectButton/SelectButton.tsx deleted file mode 100644 index d39cf2239..000000000 --- a/packages/wallet-widget/src/components/SelectButton/SelectButton.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Card, cn } from '@0xsequence/design-system' -import React, { ReactNode } from 'react' - -import { SelectedIndicator } from './SelectedIndicator' - -export interface SelectButtonProps { - children?: ReactNode - className?: string - onClick: (value: any) => void - value: any - selected: boolean - disabled?: boolean - hideIndicator?: boolean - squareIndicator?: boolean -} - -export const SelectButton = (props: SelectButtonProps) => { - const { value, selected, children, disabled, onClick, className, hideIndicator, squareIndicator = false, ...rest } = props - - return ( - - - - ) -} diff --git a/packages/wallet-widget/src/components/SelectButton/SelectedIndicator.tsx b/packages/wallet-widget/src/components/SelectButton/SelectedIndicator.tsx deleted file mode 100644 index 4fda283c6..000000000 --- a/packages/wallet-widget/src/components/SelectButton/SelectedIndicator.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { CheckmarkIcon, cn } from '@0xsequence/design-system' -import { motion } from 'motion/react' -import React from 'react' - -interface SelectedIndicatorProps { - selected: boolean - squareIndicator?: boolean - className?: string -} - -export const SelectedIndicator = (props: SelectedIndicatorProps) => { - const { selected, className, squareIndicator = false } = props - return ( -
- - {squareIndicator && } - -
- ) -} diff --git a/packages/wallet-widget/src/components/SelectButton/index.ts b/packages/wallet-widget/src/components/SelectButton/index.ts deleted file mode 100644 index 63b3005bf..000000000 --- a/packages/wallet-widget/src/components/SelectButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SelectButton } from './SelectButton' diff --git a/packages/wallet-widget/src/components/SequenceWalletProvider/ProviderComponents/FiatWalletsMapProvider.tsx b/packages/wallet-widget/src/components/SequenceWalletProvider/ProviderComponents/FiatWalletsMapProvider.tsx new file mode 100644 index 000000000..24cc1263e --- /dev/null +++ b/packages/wallet-widget/src/components/SequenceWalletProvider/ProviderComponents/FiatWalletsMapProvider.tsx @@ -0,0 +1,75 @@ +import { compareAddress, ContractVerificationStatus, useWallets } from '@0xsequence/connect' +import { useGetExchangeRate, useGetCoinPrices, useGetTokenBalancesDetails } from '@0xsequence/hooks' +import { useState, ReactNode, useEffect } from 'react' +import { zeroAddress, getAddress } from 'viem' + +import { FiatWalletPair, FiatWalletsMapContextProvider } from '../../../contexts' +import { useSettings } from '../../../hooks' +import { computeBalanceFiat } from '../../../utils' + +// Define the provider component +export const FiatWalletsMapProvider = ({ children }: { children: ReactNode }) => { + const { wallets } = useWallets() + const { selectedNetworks, hideUnlistedTokens, fiatCurrency } = useSettings() + + const [fiatWalletsMap, setFiatWalletsMap] = useState([]) + + const { data: tokenBalancesData, isPending: isTokenBalancesPending } = useGetTokenBalancesDetails({ + chainIds: selectedNetworks, + filter: { + accountAddresses: wallets.map(wallet => wallet.address), + contractStatus: hideUnlistedTokens ? ContractVerificationStatus.VERIFIED : ContractVerificationStatus.ALL, + omitNativeBalances: false + } + }) + + const coinBalancesUnordered = + tokenBalancesData?.filter(b => b.contractType === 'ERC20' || compareAddress(b.contractAddress, zeroAddress)) || [] + + const { data: coinPrices = [], isPending: isCoinPricesPending } = useGetCoinPrices( + coinBalancesUnordered.map(token => ({ + chainId: token.chainId, + contractAddress: token.contractAddress + })) + ) + + const { data: conversionRate, isPending: isConversionRatePending } = useGetExchangeRate(fiatCurrency.symbol) + + useEffect(() => { + if ( + !isTokenBalancesPending && + !isCoinPricesPending && + !isConversionRatePending && + coinBalancesUnordered.length > 0 && + coinPrices.length > 0 && + conversionRate + ) { + const newFiatWalletsMap = wallets.map(wallet => { + const walletBalances = coinBalancesUnordered.filter(b => getAddress(b.accountAddress) === getAddress(wallet.address)) + const walletFiatValue = walletBalances.reduce((acc, coin) => { + return ( + acc + + Number( + computeBalanceFiat({ + balance: coin, + prices: coinPrices, + conversionRate, + decimals: coin.contractInfo?.decimals || 18 + }) + ) + ) + }, 0) + return { + accountAddress: wallet.address, + fiatValue: walletFiatValue.toFixed(2) + } as FiatWalletPair + }) + + if (JSON.stringify(newFiatWalletsMap) !== JSON.stringify(fiatWalletsMap)) { + setFiatWalletsMap(newFiatWalletsMap) + } + } + }, [coinBalancesUnordered, coinPrices, conversionRate]) + + return {children} +} diff --git a/packages/wallet-widget/src/components/SequenceWalletProvider/ProviderComponents/SwapProvider.tsx b/packages/wallet-widget/src/components/SequenceWalletProvider/ProviderComponents/SwapProvider.tsx new file mode 100644 index 000000000..ba4ae4448 --- /dev/null +++ b/packages/wallet-widget/src/components/SequenceWalletProvider/ProviderComponents/SwapProvider.tsx @@ -0,0 +1,262 @@ +import { SwapQuote } from '@0xsequence/api' +import { getNativeTokenInfoByChainId, sendTransactions } from '@0xsequence/connect' +import { compareAddress, useToast } from '@0xsequence/design-system' +import { useAPIClient, useIndexerClient } from '@0xsequence/hooks' +import { ReactNode, useEffect, useState } from 'react' +import { formatUnits, Hex, zeroAddress } from 'viem' +import { useAccount, useChainId, useChains, usePublicClient, useWalletClient } from 'wagmi' + +import { SwapContextProvider } from '../../../contexts/Swap' +import { useNavigation } from '../../../hooks/useNavigation' +import { TokenBalanceWithPrice } from '../../../utils' + +export const SwapProvider = ({ children }: { children: ReactNode }) => { + const toast = useToast() + const { address: userAddress, connector } = useAccount() + const { setNavigation } = useNavigation() + const apiClient = useAPIClient() + const connectedChainId = useChainId() + const chains = useChains() + const [fromCoin, _setFromCoin] = useState() + const [toCoin, _setToCoin] = useState() + const [amount, _setAmount] = useState(0) + const [nonRecentAmount, setNonRecentAmount] = useState(0) + const [recentInput, setRecentInput] = useState<'from' | 'to'>('from') + + const [isSwapReady, setIsSwapReady] = useState(false) + const [swapQuoteData, setSwapQuoteData] = useState() + const [isSwapQuotePending, setIsSwapQuotePending] = useState(false) + const [hasInsufficientFunds, setHasInsufficientFunds] = useState(false) + const [isErrorSwapQuote, setIsErrorSwapQuote] = useState(false) + + const [isTxnPending, setIsTxnPending] = useState(false) + const [isErrorTxn, setIsErrorTxn] = useState(false) + + const publicClient = usePublicClient({ chainId: connectedChainId }) + const { data: walletClient } = useWalletClient({ chainId: connectedChainId }) + const indexerClient = useIndexerClient(connectedChainId) + + const resetSwapStates = () => { + _setFromCoin(undefined) + _setToCoin(undefined) + _setAmount(0) + setNonRecentAmount(0) + setRecentInput('from') + setIsSwapReady(false) + setSwapQuoteData(undefined) + setIsSwapQuotePending(false) + setIsErrorSwapQuote(false) + setIsTxnPending(false) + setIsErrorTxn(false) + } + + useEffect(() => { + resetSwapStates() + }, [userAddress, connectedChainId]) + + useEffect(() => { + setIsSwapReady(false) + setSwapQuoteData(undefined) + setIsErrorSwapQuote(false) + }, [fromCoin, toCoin, amount]) + + useEffect(() => { + const fetchSwapQuote = async () => { + if (!fromCoin || !toCoin || amount === 0) { + return + } + + setIsSwapQuotePending(true) + setIsErrorSwapQuote(false) + + let swapQuote + try { + // swapQuote = await apiClient.getSwapQuoteV2({ + // userAddress: String(userAddress), + // buyCurrencyAddress: toCoin.contractAddress, + // sellCurrencyAddress: fromCoin.contractAddress, + // chainId: connectedChainId, + // includeApprove: true + // ...(recentInput === 'from' ? {sellAmount: String(amount)} : {buyAmount: String(amount)}) + // }) + + // TODO: use commented out code when getSwapQuoteV2 is updated to include sellAmount + + swapQuote = await apiClient.getSwapQuoteV2({ + userAddress: String(userAddress), + buyCurrencyAddress: toCoin.contractAddress, + sellCurrencyAddress: fromCoin.contractAddress, + buyAmount: String(amount), + chainId: connectedChainId, + includeApprove: true + }) + + const transactionValue = swapQuote?.swapQuote?.transactionValue || '0' + // TODO: change this to "amount" from return + + setNonRecentAmount(Number(transactionValue)) + + setSwapQuoteData(swapQuote?.swapQuote) + setIsSwapReady(true) + } catch (error) { + const hasInsufficientFunds = (error as any).code === -4 + setHasInsufficientFunds(hasInsufficientFunds) + setIsErrorSwapQuote(true) + } + setIsSwapQuotePending(false) + } + + fetchSwapQuote() + }, [fromCoin, toCoin, amount]) + + const setFromCoin = (coin: TokenBalanceWithPrice | undefined) => { + if (coin?.chainId === toCoin?.chainId && coin?.contractAddress === toCoin?.contractAddress) { + switchCoinOrder() + } else { + _setFromCoin(coin) + } + } + + const setToCoin = (coin: TokenBalanceWithPrice | undefined) => { + if (coin?.chainId === fromCoin?.chainId && coin?.contractAddress === fromCoin?.contractAddress) { + switchCoinOrder() + } else { + _setToCoin(coin) + } + } + + const setAmount = (newAmount: number, type: 'from' | 'to') => { + if (type === recentInput) { + _setAmount(newAmount) + } else { + const tempAmount = amount + setRecentInput(recentInput === 'from' ? 'to' : 'from') + _setAmount(newAmount) + setNonRecentAmount(tempAmount) + } + } + + const switchCoinOrder = () => { + const tempFrom = fromCoin + const tempTo = toCoin + _setFromCoin(tempTo) + _setToCoin(tempFrom) + setRecentInput(recentInput === 'from' ? 'to' : 'from') + } + + const onSubmitSwap = async () => { + if (isErrorSwapQuote || !userAddress || !publicClient || !walletClient || !connector || !fromCoin || !toCoin || !amount) { + console.error('Please ensure validation before allowing users to submit a swap') + return + } + + setIsErrorTxn(false) + setIsTxnPending(true) + + try { + const isSwapNativeToken = compareAddress(zeroAddress, swapQuoteData?.currencyAddress || '') + + const getSwapTransactions = () => { + if (!swapQuoteData) { + return [] + } + + const swapTransactions = [ + // Swap quote optional approve step + ...(swapQuoteData?.approveData && !isSwapNativeToken + ? [ + { + to: swapQuoteData?.currencyAddress as Hex, + data: swapQuoteData?.approveData as Hex, + chain: connectedChainId + } + ] + : []), + // Swap quote tx + { + to: swapQuoteData?.to as Hex, + data: swapQuoteData?.transactionData as Hex, + chain: connectedChainId, + ...(isSwapNativeToken + ? { + value: BigInt(swapQuoteData?.transactionValue || '0') + } + : {}) + } + ] + return swapTransactions + } + + const walletClientChainId = await walletClient.getChainId() + if (walletClientChainId !== connectedChainId) { + await walletClient.switchChain({ id: connectedChainId }) + } + + const isFromCoinNative = fromCoin.contractType === 'NATIVE' + const isToCoinNative = toCoin.contractType === 'NATIVE' + const fromCoinNativeInfo = getNativeTokenInfoByChainId(fromCoin.chainId, chains) + const toCoinNativeInfo = getNativeTokenInfoByChainId(toCoin.chainId, chains) + const toastFromCoinName = isFromCoinNative ? fromCoinNativeInfo.symbol : fromCoin.contractInfo?.symbol + const toastToCoinName = isToCoinNative ? toCoinNativeInfo.symbol : toCoin.contractInfo?.symbol + const toastFromAmount = formatUnits( + BigInt(recentInput === 'from' ? amount : nonRecentAmount), + (isFromCoinNative ? fromCoinNativeInfo.decimals : fromCoin.contractInfo?.decimals) || 18 + ) + const toastToAmount = formatUnits( + BigInt(recentInput === 'from' ? nonRecentAmount : amount), + (isToCoinNative ? toCoinNativeInfo.decimals : toCoin.contractInfo?.decimals) || 18 + ) + + await sendTransactions({ + connector, + walletClient, + publicClient, + chainId: connectedChainId, + indexerClient, + senderAddress: userAddress, + transactions: [...getSwapTransactions()] + }) + + toast({ + title: 'Transaction sent', + description: `Successfully swapped ${toastFromAmount} ${toastFromCoinName} for ${toastToAmount} ${toastToCoinName}`, + variant: 'success' + }) + + setNavigation({ + location: 'home' + }) + } catch (error) { + console.error('Failed to send transactions', error) + setIsSwapReady(false) + setIsTxnPending(false) + setIsErrorTxn(true) + } + } + + return ( + + {children} + + ) +} diff --git a/packages/wallet-widget/src/components/SequenceWalletProvider/ProviderComponents/WalletConnectSignClientProvider.tsx b/packages/wallet-widget/src/components/SequenceWalletProvider/ProviderComponents/WalletConnectSignClientProvider.tsx new file mode 100644 index 000000000..9b8fae8cb --- /dev/null +++ b/packages/wallet-widget/src/components/SequenceWalletProvider/ProviderComponents/WalletConnectSignClientProvider.tsx @@ -0,0 +1,228 @@ +// import SignClient from '@walletconnect/sign-client' +// import { SessionTypes, SignClientTypes } from '@walletconnect/types' +// import { observable } from 'micro-observables' +// import { useEffect, useRef } from 'react' +// import { ReactNode } from 'react' +// import { useAccount } from 'wagmi' + +// import { WalletConnectContextProvider } from '../../../contexts/WalletConnect' +// import { useSettings } from '../../../hooks' + +// const SEQUENCE_WALLET_PROJECT_ID = '9de6e0953fc26670a19deea966e9ae1f' + +// interface WalletConnectProviderProps { +// children: ReactNode +// } + +// export interface ConnectOptions { +// app: string +// origin: string +// networkId: string +// keepWalletOpened: boolean +// } + +// export const WalletConnectProvider: React.FC = ({ children }) => { +// const { address } = useAccount() +// const { selectedNetworks } = useSettings() + +// const isReadyObservable = observable(false) +// const sessionsObservable = observable([]) +// const connectOptionsObservable = observable(undefined) + +// const currentRequestInfoRef = useRef<{ id: number; topic: string } | undefined>(undefined) +// const signClientRef = useRef(undefined) + +// useEffect(() => { +// const createSignClient = async () => { +// const client = await SignClient.init({ +// projectId: SEQUENCE_WALLET_PROJECT_ID, +// metadata: { +// name: 'Sequence Wallet', +// description: 'Sequence Wallet - The Best Crypto, NFT & Web3 Wallet', +// url: 'https://sequence.app', +// icons: ['https://sequence.app/apple-touch-icon.png'] +// } +// }) + +// client.on('session_proposal', onSessionProposal) +// client.on('session_request', onSessionRequest) +// client.on('session_ping', onSessionPing) +// client.on('session_event', onSessionEvent) +// client.on('session_update', onSessionUpdate) +// client.on('session_delete', onSessionDelete) + +// signClientRef.current = client +// sessionsObservable.set(client.session.getAll() ?? []) +// isReadyObservable.set(true) +// } + +// createSignClient() +// }, []) + +// const pair = async (uri: string) => { +// if (!signClientRef.current) { +// throw new Error('WalletConnect signClient not initialized.') +// } + +// await signClientRef.current?.core.pairing.pair({ uri }) +// } + +// const rejectRequest = () => { +// if (currentRequestInfoRef.current) { +// signClientRef.current?.respond({ +// topic: currentRequestInfoRef.current.topic, +// response: { +// id: currentRequestInfoRef.current.id, +// jsonrpc: '2.0', +// error: { +// message: 'User rejected.', +// code: 4001 +// } +// } +// }) +// } +// } + +// const disconnectSession = async (topic: string) => { +// const session = signClientRef.current?.session.get(topic) + +// if (session) { +// await signClientRef.current?.engine.client.disconnect({ +// topic: session.topic, +// reason: { +// message: 'User disconnected.', +// code: 6000 +// } +// }) + +// sessionsObservable.set(signClientRef.current?.session.getAll() ?? []) +// } +// } + +// const disconnectAllSessions = async () => { +// const sessions = signClientRef.current?.session.getAll() ?? [] +// sessions.forEach(async session => { +// await signClientRef.current?.engine.client.disconnect({ +// topic: session.topic, +// reason: { +// message: 'User disconnected.', +// code: 6000 +// } +// }) +// }) + +// sessionsObservable.set([]) +// } + +// const onSessionProposal = async (ev: SignClientTypes.EventArguments['session_proposal']) => { +// console.log('onSessionProposal', ev) + +// console.log('signClientRef.current', signClientRef.current) + +// const requiredNamespaces = ev.params.requiredNamespaces +// const optionalNamespaces = ev.params.optionalNamespaces + +// const chainsInRequiredNamespaces = +// Object.keys(requiredNamespaces).length === 0 ? [] : (requiredNamespaces.eip155.chains ?? []) +// const chainsInOptionalNamespaces = +// Object.keys(optionalNamespaces).length === 0 ? [] : (optionalNamespaces.eip155.chains ?? []) + +// const chainId = chainsInRequiredNamespaces[0]?.split(':').pop() ?? chainsInOptionalNamespaces[0]?.split(':').pop() + +// if (!chainId) { +// throw new Error('No chainId found in WalletConnect session proposal namespaces.') +// } + +// connectOptionsObservable.set({ +// app: ev.params.proposer.metadata.name, +// origin: ev.params.proposer.metadata.url, +// networkId: chainId, +// keepWalletOpened: true +// }) + +// const chains = selectedNetworks +// const requestedChains = chainsInRequiredNamespaces.map(chain => Number(chain.split(':').pop())) +// const optionalChains = chainsInOptionalNamespaces.map(chain => Number(chain.split(':').pop())) +// const filteredChainsForRequested = chains.filter(chain => [...requestedChains, ...optionalChains].includes(chain)) + +// const accounts = filteredChainsForRequested.map(chain => 'eip155:' + chain + ':' + address) + +// const namespaces = { +// eip155: { +// accounts, +// methods: [ +// 'eth_sendTransaction', +// 'eth_sendRawTransaction', +// 'eth_signTransaction', +// 'eth_sign', +// 'personal_sign', +// 'eth_signTypedData', +// 'eth_signTypedData_v4', +// 'wallet_switchEthereumChain' +// ], +// chains: [], +// events: ['chainChanged', 'accountsChanged', 'connect', 'disconnect'] +// } +// } + +// console.log('signClient', signClientRef.current) + +// const result = await signClientRef.current?.approve({ +// id: ev.id, +// namespaces +// }) + +// await result?.acknowledged() + +// sessionsObservable.set(signClientRef.current?.session.getAll() ?? []) + +// // remove any old pairings with same peerMetadata url +// signClientRef.current?.core.pairing +// .getPairings() +// .filter(pairing => ev.params.pairingTopic !== pairing.topic) +// .forEach(async pairing => { +// if (ev.params.proposer.metadata.url === pairing.peerMetadata?.url) { +// await signClientRef.current?.core.pairing.disconnect({ +// topic: pairing.topic +// }) +// } +// }) +// } + +// const onSessionRequest = async (ev: SignClientTypes.EventArguments['session_request']) => { +// console.log('onSessionRequest', ev) +// // Handle session request logic here +// } + +// const onSessionPing = async (ev: SignClientTypes.EventArguments['session_ping']) => { +// console.log('onSessionPing', ev) +// } + +// const onSessionEvent = async (ev: SignClientTypes.EventArguments['session_event']) => { +// console.log('onSessionEvent', ev) +// } + +// const onSessionUpdate = async (ev: SignClientTypes.EventArguments['session_update']) => { +// console.log('onSessionUpdate', ev) +// } + +// const onSessionDelete = async (ev: SignClientTypes.EventArguments['session_delete']) => { +// console.log('onSessionDelete', ev) +// } + +// return ( +// +// {children} +// +// ) +// } diff --git a/packages/wallet-widget/src/components/SequenceWalletProvider/SequenceWalletProvider.tsx b/packages/wallet-widget/src/components/SequenceWalletProvider/SequenceWalletProvider.tsx index add2f2495..60d6ae0ca 100644 --- a/packages/wallet-widget/src/components/SequenceWalletProvider/SequenceWalletProvider.tsx +++ b/packages/wallet-widget/src/components/SequenceWalletProvider/SequenceWalletProvider.tsx @@ -1,13 +1,19 @@ 'use client' -import { getModalPositionCss, useTheme, ShadowRoot } from '@0xsequence/connect' -import { Modal, Scroll } from '@0xsequence/design-system' +import { SequenceCheckoutProvider, useAddFundsModal } from '@0xsequence/checkout' +import { getModalPositionCss, useTheme, ShadowRoot, useOpenConnectModal } from '@0xsequence/connect' +import { Modal, Scroll, ToastProvider } from '@0xsequence/design-system' import { AnimatePresence } from 'motion/react' -import React, { useState } from 'react' +import React, { useState, useContext, useEffect } from 'react' +import { useAccount } from 'wagmi' -import { HEADER_HEIGHT } from '../../constants' +import { HEADER_HEIGHT, HEADER_HEIGHT_WITH_LABEL } from '../../constants' +import { WALLET_WIDTH, WALLET_HEIGHT } from '../../constants' import { History, Navigation, NavigationContextProvider, WalletModalContextProvider, WalletOptions } from '../../contexts' +import { WalletContentRefProvider, WalletContentRefContext } from '../../contexts/WalletContentRef' +import { FiatWalletsMapProvider } from './ProviderComponents/FiatWalletsMapProvider' +import { SwapProvider } from './ProviderComponents/SwapProvider' import { getHeader, getContent } from './utils' export type SequenceWalletProviderProps = { @@ -18,8 +24,27 @@ const DEFAULT_LOCATION: Navigation = { location: 'home' } -export const SequenceWalletProvider = ({ children }: SequenceWalletProviderProps) => { +export const SequenceWalletProvider = (props: SequenceWalletProviderProps) => { + return ( + + + + + + ) +} + +export const WalletContent = ({ children }: SequenceWalletProviderProps) => { const { theme, position } = useTheme() + const { isAddFundsModalOpen } = useAddFundsModal() + const { isConnectModalOpen } = useOpenConnectModal() + const { address } = useAccount() + + useEffect(() => { + if (!address) { + setOpenWalletModal(false) + } + }, [address]) // Wallet Modal Context const [openWalletModal, setOpenWalletModalState] = useState(false) @@ -37,48 +62,71 @@ export const SequenceWalletProvider = ({ children }: SequenceWalletProviderProps const navigation = history.length > 0 ? history[history.length - 1] : DEFAULT_LOCATION const displayScrollbar = - navigation.location === 'home' || - navigation.location === 'collection-details' || + navigation.location === 'send-general' || navigation.location === 'collectible-details' || navigation.location === 'coin-details' || navigation.location === 'history' || navigation.location === 'search' || navigation.location === 'search-view-all' || - navigation.location === 'settings-currency' + navigation.location === 'settings-wallets' || + navigation.location === 'settings-networks' || + navigation.location === 'settings-currency' || + navigation.location === 'settings-profiles' || + navigation.location === 'settings-apps' || + navigation.location === 'legacy-settings-currency' || + navigation.location === 'search-tokens' || + navigation.location === 'search-collectibles' + + let paddingTop = '0px' + switch (navigation.location) { + case 'send-general': + paddingTop = HEADER_HEIGHT_WITH_LABEL + break + default: + paddingTop = HEADER_HEIGHT + } + + const walletContentRef = useContext(WalletContentRefContext) return ( - - - {openWalletModal && ( - setOpenWalletModal(false)} - > -
- {getHeader(navigation)} - - {displayScrollbar ? ( - {getContent(navigation)} - ) : ( - getContent(navigation) + + + + + + {openWalletModal && !isAddFundsModalOpen && !isConnectModalOpen && ( + setOpenWalletModal(false)} + > +
+ {getHeader(navigation)} + + {displayScrollbar ? ( + {getContent(navigation)} + ) : ( + getContent(navigation) + )} +
+
)} -
-
- )} -
-
- {children} + + + {children} + + +
) diff --git a/packages/wallet-widget/src/components/SequenceWalletProvider/utils/index.tsx b/packages/wallet-widget/src/components/SequenceWalletProvider/utils/index.tsx index 93d1d4ff1..e2814a6f9 100644 --- a/packages/wallet-widget/src/components/SequenceWalletProvider/utils/index.tsx +++ b/packages/wallet-widget/src/components/SequenceWalletProvider/utils/index.tsx @@ -1,24 +1,27 @@ -import React from 'react' - import { Navigation } from '../../../contexts' import { CoinDetails, CollectibleDetails, - CollectionDetails, Home, Receive, SendCoin, SendCollectible, History, - SearchWallet, - SearchWalletViewAll, - SettingsMenu, - SettingsCurrency, - SettingsNetwork, - SettingsGeneral, TransactionDetails, SwapCoin, - SwapList + SwapList, + SendGeneral, + SearchTokens, + SearchCollectibles, + SettingsWallets, + SettingsApps, + SettingsCurrency, + SettingsMenu, + SettingsNetworks, + SettingsPreferences, + SettingsProfiles, + QrScan, + Swap } from '../../../views' import { NavigationHeader } from '../../NavigationHeader' import { WalletHeader } from '../../WalletHeader' @@ -27,6 +30,8 @@ export const getContent = (navigation: Navigation) => { const { location } = navigation switch (location) { + case 'send-general': + return case 'send-coin': return case 'send-collectible': @@ -37,34 +42,50 @@ export const getContent = (navigation: Navigation) => { tokenId={navigation.params.tokenId} /> ) + case 'swap': + return case 'receive': return case 'history': return - case 'search': - return - case 'search-view-all': - return + case 'search-tokens': + return + case 'search-collectibles': + return case 'settings': return - case 'settings-general': - return + case 'settings-wallets': + return + case 'settings-networks': + return case 'settings-currency': return - case 'settings-networks': - return + case 'settings-profiles': + return + case 'settings-preferences': + return + case 'settings-apps': + return + case 'connect-dapp': + return case 'coin-details': - return + return ( + + ) + case 'collectible-details': return ( ) - case 'collection-details': - return case 'transaction-details': return case 'swap-coin': @@ -77,6 +98,7 @@ export const getContent = (navigation: Navigation) => { amount={navigation.params.amount} /> ) + case 'home': default: return } @@ -85,35 +107,46 @@ export const getContent = (navigation: Navigation) => { export const getHeader = (navigation: Navigation) => { const { location } = navigation switch (location) { - case 'search': - return - case 'search-view-all': - return + case 'search-tokens': + return + case 'search-collectibles': + return case 'settings': - return - case 'settings-general': - return - case 'settings-currency': - return + return + case 'settings-wallets': + return case 'settings-networks': - return - case 'receive': - return + return + case 'settings-currency': + return + case 'settings-profiles': + return + case 'settings-preferences': + return + case 'settings-apps': + return + case 'connect-dapp': + return case 'history': - return + return case 'coin-details': return case 'collectible-details': return case 'transaction-details': return - case 'send-collectible': + case 'send-general': + return case 'send-coin': - return + return + case 'send-collectible': + return + case 'swap': + return case 'swap-coin': case 'swap-coin-list': return - default: - return + case 'receive': + return } } diff --git a/packages/wallet-widget/src/components/TransactionConfirmation.tsx b/packages/wallet-widget/src/components/TransactionConfirmation.tsx index bf30d32fd..4b52f6157 100644 --- a/packages/wallet-widget/src/components/TransactionConfirmation.tsx +++ b/packages/wallet-widget/src/components/TransactionConfirmation.tsx @@ -24,6 +24,7 @@ interface TransactionConfirmationProps { options: FeeOption[] chainId: number } + disabled?: boolean onSelectFeeOption?: (feeTokenAddress: string | null) => void isLoading?: boolean @@ -89,6 +90,7 @@ export const TransactionConfirmation = ({ feeOptions, onSelectFeeOption, isLoading, + disabled, onConfirm, onCancel }: TransactionConfirmationProps) => { @@ -103,7 +105,7 @@ export const TransactionConfirmation = ({ // If feeOptions exist and have options, a selection is required // If feeOptions don't exist or have no options, no selection is required const isFeeSelectionRequired = Boolean(feeOptions?.options?.length) - const isConfirmDisabled = isFeeSelectionRequired && !selectedFeeOptionAddress + const isConfirmDisabled = (isFeeSelectionRequired && !selectedFeeOptionAddress) || disabled return (
@@ -164,16 +166,18 @@ export const TransactionConfirmation = ({
) : ( <> +
diff --git a/packages/wallet-widget/src/components/TransactionHistoryList/TransactionHistoryItem.tsx b/packages/wallet-widget/src/components/TransactionHistoryList/TransactionHistoryItem.tsx index cdc988bc7..f2bca266a 100644 --- a/packages/wallet-widget/src/components/TransactionHistoryList/TransactionHistoryItem.tsx +++ b/packages/wallet-widget/src/components/TransactionHistoryList/TransactionHistoryItem.tsx @@ -1,10 +1,9 @@ import { TokenPrice } from '@0xsequence/api' import { compareAddress, formatDisplay, getNativeTokenInfoByChainId } from '@0xsequence/connect' -import { ArrowRightIcon, Text, Image, TransactionIcon, Skeleton, NetworkImage } from '@0xsequence/design-system' +import { ArrowRightIcon, Text, TransactionIcon, Skeleton, NetworkImage, TokenImage } from '@0xsequence/design-system' import { useGetCoinPrices, useGetExchangeRate } from '@0xsequence/hooks' import { Transaction, TxnTransfer, TxnTransferType } from '@0xsequence/indexer' import dayjs from 'dayjs' -import React from 'react' import { formatUnits, zeroAddress } from 'viem' import { useConfig } from 'wagmi' @@ -103,7 +102,15 @@ export const TransactionHistoryItem = ({ transaction }: TransactionHistoryItemPr textColor = 'positive' } - return {`${sign}${amount} ${symbol}`} + return ( + {`${sign}${amount} ${symbol}`} + ) } interface GetTransfer { @@ -144,7 +151,11 @@ export const TransactionHistoryItem = ({ transaction }: TransactionHistoryItemPr decimals = isNativeToken ? nativeTokenInfo.decimals : transfer.contractInfo?.decimals } const amountValue = formatUnits(BigInt(amount), decimals || 18) - const symbol = isNativeToken ? nativeTokenInfo.symbol : transfer.contractInfo?.symbol || '' + const symbol = isNativeToken + ? nativeTokenInfo.symbol + : isCollectible + ? transfer.contractInfo?.name || '' + : transfer.contractInfo?.symbol || '' const tokenLogoUri = isNativeToken ? nativeTokenInfo.logoURI : transfer.contractInfo?.logoURI const fiatConversionRate = coinPrices.find((coinPrice: TokenPrice) => @@ -153,8 +164,8 @@ export const TransactionHistoryItem = ({ transaction }: TransactionHistoryItemPr return (
-
- {tokenLogoUri && token logo} +
+ {(tokenLogoUri || symbol) && } {getTransferAmountLabel(decimals === 0 ? amount : formatDisplay(amountValue), symbol, transfer.transferType)}
{isPending && } diff --git a/packages/wallet-widget/src/components/WalletAccountGradient.tsx b/packages/wallet-widget/src/components/WalletAccountGradient.tsx new file mode 100644 index 000000000..6d44c5d88 --- /dev/null +++ b/packages/wallet-widget/src/components/WalletAccountGradient.tsx @@ -0,0 +1,41 @@ +import { useWallets } from '@0xsequence/connect' +import { GradientAvatar } from '@0xsequence/design-system' + +import { getConnectorLogo } from './ConnectorLogos/getConnectorLogos' + +export const WalletAccountGradient = ({ + accountAddress, + size = 'large' +}: { + accountAddress: string + size?: 'small' | 'large' +}) => { + const { wallets } = useWallets() + const remSize = size === 'small' ? 8 : 16 + + const LoginIcon = getConnectorLogo(wallets.find(wallet => wallet.address === accountAddress)?.signInMethod || '') + + return ( +
+
+ +
+
{LoginIcon}
+
+
+
+ ) +} diff --git a/packages/wallet-widget/src/components/WalletHeader/components/AccountInformation.tsx b/packages/wallet-widget/src/components/WalletHeader/components/AccountInformation.tsx index b9573b7f9..3367a2d6a 100644 --- a/packages/wallet-widget/src/components/WalletHeader/components/AccountInformation.tsx +++ b/packages/wallet-widget/src/components/WalletHeader/components/AccountInformation.tsx @@ -1,31 +1,25 @@ import { formatAddress } from '@0xsequence/connect' -import { Text, GradientAvatar, ChevronDownIcon } from '@0xsequence/design-system' -import React, { forwardRef } from 'react' +import { Text, GradientAvatar, ChevronUpDownIcon, Card } from '@0xsequence/design-system' import { useAccount } from 'wagmi' interface AccountInformationProps { - onClickAccount: () => void + onClickAccount?: () => void } -export const AccountInformation = forwardRef(({ onClickAccount }: AccountInformationProps, ref) => { +export const AccountInformation = ({ onClickAccount }: AccountInformationProps) => { const { address } = useAccount() return ( -
-
-
- - - {formatAddress(address || '')} - - -
-
-
+ + + + {formatAddress(address || '')} + + {onClickAccount && } + ) -}) +} diff --git a/packages/wallet-widget/src/components/WalletHeader/components/WalletDropdownContent.tsx b/packages/wallet-widget/src/components/WalletHeader/components/WalletDropdownContent.tsx deleted file mode 100644 index c790f9e95..000000000 --- a/packages/wallet-widget/src/components/WalletHeader/components/WalletDropdownContent.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { formatAddress, useTheme } from '@0xsequence/connect' -import { - Button, - IconButton, - CloseIcon, - GradientAvatar, - Text, - QrCodeIcon, - SettingsIcon, - SignoutIcon, - TransactionIcon -} from '@0xsequence/design-system' -import React, { forwardRef } from 'react' -import { useDisconnect, useAccount } from 'wagmi' - -import { useNavigation } from '../../../hooks' -import { useOpenWalletModal } from '../../../hooks/useOpenWalletModal' -import { CopyButton } from '../../CopyButton' - -interface WalletDropdownContentProps { - setOpenWalletDropdown: React.Dispatch> -} - -export const WalletDropdownContent = forwardRef(({ setOpenWalletDropdown }: WalletDropdownContentProps, ref: any) => { - const { setNavigation } = useNavigation() - const { setOpenWalletModal } = useOpenWalletModal() - const { address } = useAccount() - const { disconnect } = useDisconnect() - const { theme } = useTheme() - const onClickReceive = () => { - setOpenWalletDropdown(false) - setNavigation({ - location: 'receive' - }) - } - - const onClickHistory = () => { - setOpenWalletDropdown(false) - setNavigation({ - location: 'history' - }) - } - - const onClickSettings = () => { - setOpenWalletDropdown(false) - setNavigation({ - location: 'settings' - }) - } - - const onClickSignout = () => { - setOpenWalletModal(false) - setOpenWalletDropdown(false) - disconnect() - } - - const getDropdownBackgroundColor = () => { - switch (theme) { - case 'dark': - return 'rgba(38, 38, 38, 0.85)' - case 'light': - return 'rgba(217, 217, 217, 0.85)' - default: - return 'transparent' - } - } - - return ( -
-
-
- - - {formatAddress(address || '')} - - -
- setOpenWalletDropdown(false)} size="xs" icon={CloseIcon} /> -
-
-
-
- ) -}) diff --git a/packages/wallet-widget/src/components/WalletHeader/index.tsx b/packages/wallet-widget/src/components/WalletHeader/index.tsx index d92919d99..ca06c062d 100644 --- a/packages/wallet-widget/src/components/WalletHeader/index.tsx +++ b/packages/wallet-widget/src/components/WalletHeader/index.tsx @@ -1,84 +1,80 @@ -import { ChevronLeftIcon, IconButton, SearchIcon } from '@0xsequence/design-system' -import * as PopoverPrimitive from '@radix-ui/react-popover' -import { AnimatePresence, motion } from 'motion/react' -import React, { useState, useRef, useEffect } from 'react' +import { useOpenConnectModal, useWallets } from '@0xsequence/connect' +import { ChevronLeftIcon, IconButton, Text } from '@0xsequence/design-system' +import { AnimatePresence } from 'motion/react' +import { useState } from 'react' -import { HEADER_HEIGHT } from '../../constants' -import { useNavigation, useOpenWalletModal } from '../../hooks' +import { HEADER_HEIGHT, HEADER_HEIGHT_WITH_LABEL } from '../../constants' +import { useNavigation } from '../../hooks' +import { SelectWalletRow } from '../Select/SelectWalletRow' +import { SlideupDrawer } from '../Select/SlideupDrawer' import { AccountInformation } from './components/AccountInformation' -import { WalletDropdownContent } from './components/WalletDropdownContent' -export const WalletHeader = () => { - const { openWalletModalState } = useOpenWalletModal() +export const WalletHeader = ({ + primaryText, + enableAccountSelector = false +}: { + primaryText?: string + enableAccountSelector?: boolean +}) => { + const { wallets, setActiveWallet } = useWallets() + const { setOpenConnectModal } = useOpenConnectModal() + const { goBack } = useNavigation() - const [openWalletDropdown, setOpenWalletDropdown] = useState(false) - const { goBack, history, setNavigation } = useNavigation() - const hasDropdownOpened = useRef(false) + const [accountSelectorModalOpen, setAccountSelectorModalOpen] = useState(false) - useEffect(() => { - if (!openWalletModalState) { - setOpenWalletDropdown(false) - } - }, [openWalletModalState]) - - // Close dropdown when navigating to a new page - useEffect(() => { - if (openWalletDropdown) { - if (!hasDropdownOpened.current) { - hasDropdownOpened.current = true - } else { - setOpenWalletDropdown(false) - } - } else { - hasDropdownOpened.current = false - } - }, [history.length, openWalletDropdown]) + const onClickSelector = () => { + setAccountSelectorModalOpen(true) + } - const onClickAccount = () => { - setOpenWalletDropdown(true) + const handleAddNewWallet = () => { + setAccountSelectorModalOpen(false) + setOpenConnectModal(true) } const onClickBack = () => { goBack() } - const onClickSearch = () => { - setNavigation({ - location: 'search' - }) - } - return ( - - - -
- {history.length > 0 ? ( - - ) : ( - - )} - - - -
-
- - - {openWalletDropdown && ( - - - - )} - - - +
+
+ + +
+
+ {primaryText && ( + + {primaryText} + + )} + + {accountSelectorModalOpen && ( + setAccountSelectorModalOpen(false)} + label="Select main wallet" + buttonLabel="+ Add new wallet" + handleButtonPress={handleAddNewWallet} + dragHandleWidth={28} + > +
+ {wallets.map((wallet, index) => ( + setAccountSelectorModalOpen(false)} + onClick={setActiveWallet} + /> + ))} +
+
+ )} +
+
) } diff --git a/packages/wallet-widget/src/constants/sizing.ts b/packages/wallet-widget/src/constants/sizing.ts index 95078b47e..1eea31b50 100644 --- a/packages/wallet-widget/src/constants/sizing.ts +++ b/packages/wallet-widget/src/constants/sizing.ts @@ -1 +1,4 @@ -export const HEADER_HEIGHT = '54px' +export const HEADER_HEIGHT = '60px' +export const HEADER_HEIGHT_WITH_LABEL = '100px' +export const WALLET_WIDTH = '460px' +export const WALLET_HEIGHT = 'min(800px, 90vh)' diff --git a/packages/wallet-widget/src/contexts/FiatWalletsMap.ts b/packages/wallet-widget/src/contexts/FiatWalletsMap.ts new file mode 100644 index 000000000..8553645b9 --- /dev/null +++ b/packages/wallet-widget/src/contexts/FiatWalletsMap.ts @@ -0,0 +1,15 @@ +import { createGenericContext } from './genericContext' + +export interface FiatWalletPair { + accountAddress: string + fiatValue: string +} + +export interface FiatWalletsMapContext { + fiatWalletsMap: FiatWalletPair[] + setFiatWalletsMap: (fiatWalletsMap: FiatWalletPair[]) => void +} + +const [useFiatWalletsMapContext, FiatWalletsMapContextProvider] = createGenericContext() + +export { useFiatWalletsMapContext, FiatWalletsMapContextProvider } diff --git a/packages/wallet-widget/src/contexts/Navigation.ts b/packages/wallet-widget/src/contexts/Navigation.ts index 71759ad51..9f837b685 100644 --- a/packages/wallet-widget/src/contexts/Navigation.ts +++ b/packages/wallet-widget/src/contexts/Navigation.ts @@ -4,19 +4,10 @@ import { Transaction } from '@0xsequence/indexer' import { createGenericContext } from './genericContext' -export interface CollectionDetailsParams { - contractAddress: string - chainId: number -} - -export interface CollectionDetailsNavigation { - location: 'collection-details' - params: CollectionDetailsParams -} - export interface CoinDetailsParams { contractAddress: string chainId: number + accountAddress: string } export interface CoinDetailsNavigation { @@ -28,6 +19,7 @@ export interface CollectibleDetailsParams { contractAddress: string chainId: number tokenId: string + accountAddress: string } export interface CollectibleDetailsNavigation { @@ -98,21 +90,31 @@ export interface SendCollectibleNavigation { export interface BasicNavigation { location: | 'home' + | 'send-general' + | 'swap' | 'receive' | 'history' - | 'receive' + | 'legacy-settings' + | 'legacy-settings-general' + | 'legacy-settings-currency' + | 'legacy-settings-networks' | 'settings' - | 'settings-general' - | 'settings-currency' + | 'settings-wallets' | 'settings-networks' + | 'settings-currency' + | 'settings-profiles' + | 'settings-apps' + | 'settings-preferences' + | 'connect-dapp' | 'search' + | 'search-tokens' + | 'search-collectibles' } export type Navigation = | BasicNavigation | CoinDetailsNavigation | CollectibleDetailsNavigation - | CollectionDetailsNavigation | TransactionDetailsNavigation | SearchViewAllNavigation | SendCoinNavigation diff --git a/packages/wallet-widget/src/contexts/Swap.ts b/packages/wallet-widget/src/contexts/Swap.ts new file mode 100644 index 000000000..ed0ae1e8e --- /dev/null +++ b/packages/wallet-widget/src/contexts/Swap.ts @@ -0,0 +1,27 @@ +import { TokenBalanceWithPrice } from '../utils' + +import { createGenericContext } from './genericContext' + +export interface SwapContext { + fromCoin: TokenBalanceWithPrice | undefined + toCoin: TokenBalanceWithPrice | undefined + amount: number + nonRecentAmount: number + recentInput: 'from' | 'to' + isSwapReady: boolean + isSwapQuotePending: boolean + hasInsufficientFunds: boolean + isErrorSwapQuote: boolean + isTxnPending: boolean + isErrorTxn: boolean + setFromCoin: (coin: TokenBalanceWithPrice | undefined) => void + setToCoin: (coin: TokenBalanceWithPrice | undefined) => void + setAmount: (amount: number, type: 'from' | 'to') => void + switchCoinOrder: () => void + onSubmitSwap: () => void + resetSwapStates: () => void +} + +const [useSwapContext, SwapContextProvider] = createGenericContext() + +export { useSwapContext, SwapContextProvider } diff --git a/packages/wallet-widget/src/contexts/WalletConnect.ts b/packages/wallet-widget/src/contexts/WalletConnect.ts new file mode 100644 index 000000000..f72ad2502 --- /dev/null +++ b/packages/wallet-widget/src/contexts/WalletConnect.ts @@ -0,0 +1,18 @@ +// import { SessionTypes } from '@walletconnect/types' +// import { Observable } from 'micro-observables' + +// import { ConnectOptions } from '../components/SequenceWalletProvider/utils/WalletConnectSignClient' + +// import { createGenericContext } from './genericContext' + +// export interface WalletConnectContextProps { +// isReadyObservable: Observable +// sessionsObservable: Observable +// connectOptionsObservable: Observable +// pair: (uri: string) => Promise +// rejectRequest: () => void +// disconnectSession: (topic: string) => Promise +// disconnectAllSessions: () => Promise +// } + +// export const [useWalletConnectContext, WalletConnectContextProvider] = createGenericContext() diff --git a/packages/wallet-widget/src/contexts/WalletContentRef.tsx b/packages/wallet-widget/src/contexts/WalletContentRef.tsx new file mode 100644 index 000000000..f9b9e6f4d --- /dev/null +++ b/packages/wallet-widget/src/contexts/WalletContentRef.tsx @@ -0,0 +1,11 @@ +import { createContext, useRef, RefObject } from 'react' + +const WalletContentRefContext = createContext>({ current: null }) + +const WalletContentRefProvider = ({ children }: { children: React.ReactNode }) => { + const walletContentRef = useRef(null) + + return {children} +} + +export { WalletContentRefContext, WalletContentRefProvider } diff --git a/packages/wallet-widget/src/contexts/index.ts b/packages/wallet-widget/src/contexts/index.ts index 73eb035fe..cd8f5cd04 100644 --- a/packages/wallet-widget/src/contexts/index.ts +++ b/packages/wallet-widget/src/contexts/index.ts @@ -1,2 +1,3 @@ export * from './WalletModal' export * from './Navigation' +export * from './FiatWalletsMap' diff --git a/packages/wallet-widget/src/hooks/useFiatWalletsMap.ts b/packages/wallet-widget/src/hooks/useFiatWalletsMap.ts new file mode 100644 index 000000000..df0aa6818 --- /dev/null +++ b/packages/wallet-widget/src/hooks/useFiatWalletsMap.ts @@ -0,0 +1,7 @@ +import { useFiatWalletsMapContext } from '../contexts/FiatWalletsMap' + +export const useFiatWalletsMap = () => { + const { fiatWalletsMap, setFiatWalletsMap } = useFiatWalletsMapContext() + + return { fiatWalletsMap, setFiatWalletsMap } +} diff --git a/packages/wallet-widget/src/hooks/useSettings.ts b/packages/wallet-widget/src/hooks/useSettings.ts index ca6c32b5e..fbf907373 100644 --- a/packages/wallet-widget/src/hooks/useSettings.ts +++ b/packages/wallet-widget/src/hooks/useSettings.ts @@ -1,21 +1,54 @@ -import { LocalStorageKey, useWalletSettings } from '@0xsequence/connect' -import { useState } from 'react' +import { ConnectedWallet, useWallets, LocalStorageKey, useWalletSettings } from '@0xsequence/connect' +import { Observable, observable } from 'micro-observables' import { useConfig } from 'wagmi' import { FiatCurrency, defaultFiatCurrency } from '../constants' +interface MutableObservable extends Observable { + set(value: T): void +} + +export interface SettingsCollection { + contractAddress: string + chainId: number + contractInfo: { + name: string + logoURI: string + } +} + interface Settings { hideCollectibles: boolean hideUnlistedTokens: boolean fiatCurrency: FiatCurrency selectedNetworks: number[] + allNetworks: number[] + selectedWallets: ConnectedWallet[] + selectedCollections: SettingsCollection[] + hideCollectiblesObservable: Observable + hideUnlistedTokensObservable: Observable + fiatCurrencyObservable: Observable + selectedNetworksObservable: Observable + selectedWalletsObservable: Observable + selectedCollectionsObservable: Observable setFiatCurrency: (newFiatCurrency: FiatCurrency) => void setHideCollectibles: (newState: boolean) => void setHideUnlistedTokens: (newState: boolean) => void + setSelectedWallets: (newWallets: ConnectedWallet[]) => void setSelectedNetworks: (newNetworks: number[]) => void + setSelectedCollections: (newCollections: SettingsCollection[]) => void } -type SettingsItems = Pick +type SettingsItems = { + hideCollectiblesObservable: MutableObservable + hideUnlistedTokensObservable: MutableObservable + fiatCurrencyObservable: MutableObservable + selectedWalletsObservable: MutableObservable + selectedNetworksObservable: MutableObservable + selectedCollectionsObservable: MutableObservable +} + +let settingsObservables: SettingsItems | null = null /** * Hook to manage wallet settings with persistent storage. @@ -60,8 +93,9 @@ type SettingsItems = Pick { const { readOnlyNetworks, displayedAssets } = useWalletSettings() const { chains } = useConfig() + const { wallets: allWallets } = useWallets() - const allChains = [ + const allNetworks = [ ...new Set([...chains.map(chain => chain.id), ...(readOnlyNetworks || []), ...displayedAssets.map(asset => asset.chainId)]) ] @@ -69,12 +103,15 @@ export const useSettings = (): Settings => { let hideUnlistedTokens = true let hideCollectibles = false let fiatCurrency = defaultFiatCurrency - let selectedNetworks = allChains + let selectedWallets: ConnectedWallet[] = allWallets + let selectedNetworks: number[] = allNetworks + let selectedCollections: SettingsCollection[] = [] try { const settingsStorage = localStorage.getItem(LocalStorageKey.Settings) const settings = JSON.parse(settingsStorage || '{}') + if (settings?.hideUnlistedTokens !== undefined) { hideUnlistedTokens = settings?.hideUnlistedTokens } @@ -84,78 +121,160 @@ export const useSettings = (): Settings => { if (settings?.fiatCurrency !== undefined) { fiatCurrency = settings?.fiatCurrency as FiatCurrency } + if (settings?.selectedWallets !== undefined) { + selectedWallets = settings?.selectedWallets as ConnectedWallet[] + const isPartialSelection = selectedWallets.length > 1 && selectedWallets.length !== allWallets.length + const hasInvalidWallets = + selectedWallets.some(wallet => !allWallets.some((w: ConnectedWallet) => w.address === wallet.address)) || + isPartialSelection + + if (hasInvalidWallets && allWallets.length !== 0) { + selectedWallets = allWallets + localStorage.setItem(LocalStorageKey.Settings, JSON.stringify({ ...settings, selectedWallets: allWallets })) + } + } if (settings?.selectedNetworks !== undefined) { - let areSelectedNetworksValid = true + selectedNetworks = settings?.selectedNetworks as number[] + + let hasInvalidNetworks = false settings.selectedNetworks.forEach((chainId: number) => { - if (allChains.find(chain => chain === chainId) === undefined) { - areSelectedNetworksValid = false + if (allNetworks.find(chain => chain === chainId) === undefined) { + hasInvalidNetworks = true } }) - if (areSelectedNetworksValid) { - selectedNetworks = settings?.selectedNetworks as number[] + + const hasInvalidNetworksSelection = selectedNetworks.length > 1 && selectedNetworks.length !== allNetworks.length + + if (hasInvalidNetworks || hasInvalidNetworksSelection) { + selectedNetworks = allNetworks + localStorage.setItem(LocalStorageKey.Settings, JSON.stringify({ ...settings, selectedNetworks: allNetworks })) } } + if (settings?.selectedCollections !== undefined) { + selectedCollections = settings?.selectedCollections + } } catch (e) { console.error(e, 'Failed to fetch settings') } return { - hideUnlistedTokens, - hideCollectibles, - fiatCurrency, - selectedNetworks + hideUnlistedTokensObservable: observable(hideUnlistedTokens), + hideCollectiblesObservable: observable(hideCollectibles), + fiatCurrencyObservable: observable(fiatCurrency), + selectedWalletsObservable: observable(selectedWallets), + selectedNetworksObservable: observable(selectedNetworks), + selectedCollectionsObservable: observable(selectedCollections) } } - const defaultSettings = getSettingsFromStorage() - const [settings, setSettings] = useState(defaultSettings) + const resetSettings = () => { + if (settingsObservables) { + const selectedWallets = settingsObservables.selectedWalletsObservable.get() + const selectedNetworks = settingsObservables.selectedNetworksObservable.get() - const setHideUnlistedTokens = (newState: boolean) => { - const oldSettings = getSettingsFromStorage() - const newSettings = { - ...oldSettings, - hideUnlistedTokens: newState + const isPartialSelection = selectedWallets.length > 1 && selectedWallets.length !== allWallets.length + const hasInvalidWallets = + selectedWallets.some(wallet => !allWallets.some((w: ConnectedWallet) => w.address === wallet.address)) || + isPartialSelection + + const hasInvalidNetworksSelection = selectedNetworks.length > 1 && selectedNetworks.length !== allNetworks.length + + if (hasInvalidWallets || hasInvalidNetworksSelection || !selectedWallets.length) { + return true + } } - localStorage.setItem(LocalStorageKey.Settings, JSON.stringify(newSettings)) - setSettings(newSettings) + return false + } + + if (!settingsObservables || resetSettings()) { + settingsObservables = getSettingsFromStorage() + } + + const { + hideUnlistedTokensObservable, + hideCollectiblesObservable, + fiatCurrencyObservable, + selectedWalletsObservable, + selectedNetworksObservable, + selectedCollectionsObservable + } = settingsObservables + + const setHideUnlistedTokens = (newState: boolean) => { + hideUnlistedTokensObservable.set(newState) + updateLocalStorage() } const setHideCollectibles = (newState: boolean) => { - const oldSettings = getSettingsFromStorage() - const newSettings = { - ...oldSettings, - hideCollectibles: newState - } - localStorage.setItem(LocalStorageKey.Settings, JSON.stringify(newSettings)) - setSettings(newSettings) + hideCollectiblesObservable.set(newState) + updateLocalStorage() } const setFiatCurrency = (newFiatCurrency: FiatCurrency) => { - const oldSettings = getSettingsFromStorage() - const newSettings = { - ...oldSettings, - fiatCurrency: newFiatCurrency + fiatCurrencyObservable.set(newFiatCurrency) + updateLocalStorage() + } + + const setSelectedWallets = (newSelectedWallets: ConnectedWallet[]) => { + if (newSelectedWallets.length === 0) { + selectedWalletsObservable.set(allWallets) + } else { + selectedWalletsObservable.set(newSelectedWallets) } - localStorage.setItem(LocalStorageKey.Settings, JSON.stringify(newSettings)) - setSettings(newSettings) + updateLocalStorage() } const setSelectedNetworks = (newSelectedNetworks: number[]) => { - const oldSettings = getSettingsFromStorage() + if (newSelectedNetworks.length === 0) { + selectedNetworksObservable.set(allNetworks) + } else { + selectedNetworksObservable.set(newSelectedNetworks) + selectedCollectionsObservable.set([]) + } + updateLocalStorage() + } + + const setSelectedCollections = (newSelectedCollections: SettingsCollection[]) => { + if (newSelectedCollections.length === 0) { + selectedCollectionsObservable.set([]) + } else { + selectedCollectionsObservable.set(newSelectedCollections) + } + updateLocalStorage() + } + + const updateLocalStorage = () => { const newSettings = { - ...oldSettings, - selectedNetworks: newSelectedNetworks + hideUnlistedTokens: hideUnlistedTokensObservable.get(), + hideCollectibles: hideCollectiblesObservable.get(), + fiatCurrency: fiatCurrencyObservable.get(), + selectedWallets: selectedWalletsObservable.get(), + selectedNetworks: selectedNetworksObservable.get(), + selectedCollections: selectedCollectionsObservable.get() } + console.log('settings updated', newSettings) localStorage.setItem(LocalStorageKey.Settings, JSON.stringify(newSettings)) - setSettings(newSettings) } return { - ...settings, + hideUnlistedTokens: hideUnlistedTokensObservable.get(), + hideCollectibles: hideCollectiblesObservable.get(), + fiatCurrency: fiatCurrencyObservable.get(), + selectedWallets: selectedWalletsObservable.get(), + selectedNetworks: selectedNetworksObservable.get(), + allNetworks: allNetworks, + selectedCollections: selectedCollectionsObservable.get(), + hideUnlistedTokensObservable, + hideCollectiblesObservable, + fiatCurrencyObservable, + selectedWalletsObservable, + selectedNetworksObservable, + selectedCollectionsObservable, setFiatCurrency, setHideCollectibles, setHideUnlistedTokens, - setSelectedNetworks + setSelectedWallets, + setSelectedNetworks, + setSelectedCollections } } diff --git a/packages/wallet-widget/src/hooks/useSwap.tsx b/packages/wallet-widget/src/hooks/useSwap.tsx new file mode 100644 index 000000000..aa4cda67d --- /dev/null +++ b/packages/wallet-widget/src/hooks/useSwap.tsx @@ -0,0 +1,43 @@ +import { useSwapContext } from '../contexts/Swap' + +export const useSwap = () => { + const { + fromCoin, + toCoin, + amount, + nonRecentAmount, + recentInput, + isSwapReady, + isSwapQuotePending, + hasInsufficientFunds, + isErrorSwapQuote, + isTxnPending, + isErrorTxn, + setFromCoin, + setToCoin, + setAmount, + switchCoinOrder, + onSubmitSwap, + resetSwapStates + } = useSwapContext() + + return { + fromCoin, + toCoin, + amount, + nonRecentAmount, + recentInput, + isSwapReady, + isSwapQuotePending, + hasInsufficientFunds, + isErrorSwapQuote, + isTxnPending, + isErrorTxn, + setFromCoin, + setToCoin, + setAmount, + switchCoinOrder, + onSubmitSwap, + resetSwapStates + } +} diff --git a/packages/wallet-widget/src/hooks/useWalletConnect.ts b/packages/wallet-widget/src/hooks/useWalletConnect.ts new file mode 100644 index 000000000..8c6e167db --- /dev/null +++ b/packages/wallet-widget/src/hooks/useWalletConnect.ts @@ -0,0 +1,9 @@ +// import { useWalletConnectContext } from '../contexts/WalletConnect' + +// export const useWalletConnect = () => { +// const context = useWalletConnectContext() +// if (!context) { +// throw new Error('useWalletConnect must be used within a WalletConnectProvider') +// } +// return context +// } diff --git a/packages/wallet-widget/src/utils/formatBalance.ts b/packages/wallet-widget/src/utils/formatBalance.ts new file mode 100644 index 000000000..520350099 --- /dev/null +++ b/packages/wallet-widget/src/utils/formatBalance.ts @@ -0,0 +1,74 @@ +import { formatDisplay } from '@0xsequence/connect' +import { getNativeTokenInfoByChainId } from '@0xsequence/connect' +import { compareAddress } from '@0xsequence/design-system' +import { TokenBalance } from '@0xsequence/indexer' +import { Chain, formatUnits } from 'viem' +import { zeroAddress } from 'viem' + +import { TokenBalanceWithPrice } from './tokens' + +//TODO: rename these and maybe do a refactor + +export const formatTokenInfo = ( + balance: TokenBalanceWithPrice | undefined, + fiatSign: string, + chains: readonly [Chain, ...Chain[]] +) => { + if (!balance) { + return { logo: '', name: '', symbol: '', displayBalance: '', fiatBalance: '' } + } + + const isNativeToken = compareAddress(balance?.contractAddress || '', zeroAddress) + const nativeTokenInfo = getNativeTokenInfoByChainId(balance?.chainId || 1, chains) + + const selectedCoinLogo = isNativeToken ? nativeTokenInfo.logoURI : balance?.contractInfo?.logoURI + const selectedCoinName = isNativeToken ? nativeTokenInfo.name : balance?.contractInfo?.name || 'Unknown' + const selectedCoinSymbol = isNativeToken ? nativeTokenInfo.symbol : balance?.contractInfo?.symbol + + const decimals = isNativeToken ? nativeTokenInfo.decimals : balance?.contractInfo?.decimals + const bal = formatUnits(BigInt(balance?.balance || 0), decimals || 18) + const displayBalance = formatDisplay(bal) + const symbol = isNativeToken ? nativeTokenInfo.symbol : balance?.contractInfo?.symbol + + return { + isNativeToken, + nativeTokenInfo, + logo: selectedCoinLogo, + name: selectedCoinName, + symbol: selectedCoinSymbol, + displayBalance: `${displayBalance} ${symbol}`, + fiatBalance: `${fiatSign}${(balance.price.value * Number(bal)).toFixed(2)}` + } +} + +export const formatFiatBalance = (balance: number, price: number, decimals: number, fiatSign: string) => { + if (!balance) { + return '' + } + + const bal = formatUnits(BigInt(Number(balance)), decimals || 18) + + return `${fiatSign}${(price * Number(bal)).toFixed(2)}` +} + +export const formatTokenUnits = (token: TokenBalance | undefined, chains: readonly [Chain, ...Chain[]]) => { + if (!token) { + return '' + } + + const isNativeToken = token.contractType === 'NATIVE' + const nativeTokenInfo = getNativeTokenInfoByChainId(token.chainId, chains) + + if (isNativeToken) { + return formatUnits(BigInt(Number(token.balance)), nativeTokenInfo.decimals) + } + return formatUnits(BigInt(Number(token.balance)), token.contractInfo?.decimals || 18) +} + +export const decimalsToWei = (balance: number, decimals: number) => { + const scaledBalance = balance * Math.pow(10, decimals) + + const balanceBigInt = BigInt(scaledBalance) + + return Number(balanceBigInt) +} diff --git a/packages/wallet-widget/src/utils/tokens.ts b/packages/wallet-widget/src/utils/tokens.ts index d4c7b5fe2..3642ae695 100644 --- a/packages/wallet-widget/src/utils/tokens.ts +++ b/packages/wallet-widget/src/utils/tokens.ts @@ -1,9 +1,14 @@ -import { TokenPrice } from '@0xsequence/api' +import { Price, TokenPrice } from '@0xsequence/api' import { compareAddress } from '@0xsequence/connect' import { TokenBalance, GetTransactionHistoryReturn, Transaction } from '@0xsequence/indexer' -import { InfiniteData } from '@tanstack/react-query' +import { InfiniteData, UseInfiniteQueryResult } from '@tanstack/react-query' +import { useInfiniteQuery } from '@tanstack/react-query' import { formatUnits, zeroAddress } from 'viem' +export interface TokenBalanceWithPrice extends TokenBalance { + price: Price +} + export const getPercentageColor = (value: number) => { if (value > 0) { return 'positive' @@ -34,7 +39,9 @@ interface ComputeBalanceFiat { export const computeBalanceFiat = ({ balance, prices, decimals, conversionRate }: ComputeBalanceFiat): string => { let totalUsd = 0 - const priceForToken = prices.find(p => compareAddress(p.token.contractAddress, balance.contractAddress)) + const priceForToken = prices.find( + p => compareAddress(p.token.contractAddress, balance.contractAddress) && p.token.chainId === balance.chainId + ) if (!priceForToken) { return '0.00' } @@ -96,3 +103,25 @@ export const flattenPaginatedTransactionHistory = ( return transactionHistory } + +export const useGetMoreBalances = ( + balances: TokenBalanceWithPrice[], + pageSize: number, + options?: { enabled: boolean } +): UseInfiniteQueryResult, Error> => { + return useInfiniteQuery({ + queryKey: ['infiniteBalances', balances], + queryFn: ({ pageParam }) => { + const startIndex = pageParam * pageSize + return balances.slice(startIndex, startIndex + pageSize) + }, + getNextPageParam: (lastPage, allPages) => { + if (lastPage.length < pageSize) { + return undefined + } + return allPages.length + }, + initialPageParam: 0, + enabled: !!balances.length && (options?.enabled ?? true) + }) +} diff --git a/packages/wallet-widget/src/views/CoinDetails/index.tsx b/packages/wallet-widget/src/views/CoinDetails/index.tsx index b72aa6b2d..9d1b47f89 100644 --- a/packages/wallet-widget/src/views/CoinDetails/index.tsx +++ b/packages/wallet-widget/src/views/CoinDetails/index.tsx @@ -1,8 +1,15 @@ -import { compareAddress, formatDisplay, getNativeTokenInfoByChainId, ContractVerificationStatus } from '@0xsequence/connect' +import { + compareAddress, + formatDisplay, + getNativeTokenInfoByChainId, + ContractVerificationStatus, + useWallets +} from '@0xsequence/connect' import { Button, SendIcon, SwapIcon, Text, TokenImage } from '@0xsequence/design-system' import { useGetTokenBalancesSummary, useGetCoinPrices, useGetExchangeRate, useGetTransactionHistory } from '@0xsequence/hooks' +import { useEffect } from 'react' import { formatUnits, zeroAddress } from 'viem' -import { useAccount, useConfig } from 'wagmi' +import { useConfig } from 'wagmi' import { InfiniteScroll } from '../../components/InfiniteScroll' import { NetworkBadge } from '../../components/NetworkBadge' @@ -16,13 +23,18 @@ import { CoinDetailsSkeleton } from './Skeleton' export interface CoinDetailsProps { contractAddress: string chainId: number + accountAddress: string } -export const CoinDetails = ({ contractAddress, chainId }: CoinDetailsProps) => { +export const CoinDetails = ({ contractAddress, chainId, accountAddress }: CoinDetailsProps) => { const { chains } = useConfig() const { setNavigation } = useNavigation() + const { setActiveWallet } = useWallets() const { fiatCurrency, hideUnlistedTokens } = useSettings() - const { address: accountAddress } = useAccount() + + useEffect(() => { + setActiveWallet(accountAddress) + }, [accountAddress]) const isReadOnly = !chains.map(chain => chain.id).includes(chainId) @@ -85,7 +97,7 @@ export const CoinDetails = ({ contractAddress, chainId }: CoinDetailsProps) => { balance: dataCoinBalance, prices: dataCoinPrices || [], conversionRate, - decimals: decimals || 0 + decimals: decimals || 18 }) : '0' diff --git a/packages/wallet-widget/src/views/CollectibleDetails/index.tsx b/packages/wallet-widget/src/views/CollectibleDetails/index.tsx index 4034b1109..e5853e2ba 100644 --- a/packages/wallet-widget/src/views/CollectibleDetails/index.tsx +++ b/packages/wallet-widget/src/views/CollectibleDetails/index.tsx @@ -1,4 +1,4 @@ -import { formatDisplay, ContractVerificationStatus } from '@0xsequence/connect' +import { formatDisplay, ContractVerificationStatus, useWallets } from '@0xsequence/connect' import { Button, Image, NetworkImage, SendIcon, Text } from '@0xsequence/design-system' import { useGetTokenBalancesDetails, @@ -6,8 +6,9 @@ import { useGetCollectiblePrices, useGetExchangeRate } from '@0xsequence/hooks' +import { useEffect } from 'react' import { formatUnits } from 'viem' -import { useAccount, useConfig } from 'wagmi' +import { useConfig } from 'wagmi' import { CollectibleTileImage } from '../../components/CollectibleTileImage' import { InfiniteScroll } from '../../components/InfiniteScroll' @@ -17,20 +18,23 @@ import { useSettings, useNavigation } from '../../hooks' import { computeBalanceFiat, flattenPaginatedTransactionHistory } from '../../utils' import { CollectibleDetailsSkeleton } from './Skeleton' - export interface CollectibleDetailsProps { contractAddress: string chainId: number tokenId: string + accountAddress: string } -export const CollectibleDetails = ({ contractAddress, chainId, tokenId }: CollectibleDetailsProps) => { +export const CollectibleDetails = ({ contractAddress, chainId, tokenId, accountAddress }: CollectibleDetailsProps) => { const { chains } = useConfig() - - const { address: accountAddress } = useAccount() + const { setActiveWallet } = useWallets() const { fiatCurrency } = useSettings() const { setNavigation } = useNavigation() + useEffect(() => { + setActiveWallet(accountAddress || '') + }, [accountAddress]) + const isReadOnly = !chains.map(chain => chain.id).includes(chainId) const { diff --git a/packages/wallet-widget/src/views/CollectionDetails/Skeleton.tsx b/packages/wallet-widget/src/views/CollectionDetails/Skeleton.tsx deleted file mode 100644 index c94e79228..000000000 --- a/packages/wallet-widget/src/views/CollectionDetails/Skeleton.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Skeleton } from '@0xsequence/design-system' -import React from 'react' - -import { NetworkBadge } from '../../components/NetworkBadge' - -interface CollectionDetailsSkeletonProps { - chainId: number -} - -export const CollectionDetailsSkeleton = ({ chainId }: CollectionDetailsSkeletonProps) => { - return ( -
-
- - - - -
-
- -
- {Array(8) - .fill(null) - .map((_, i) => ( - - ))} -
-
-
- ) -} diff --git a/packages/wallet-widget/src/views/CollectionDetails/index.tsx b/packages/wallet-widget/src/views/CollectionDetails/index.tsx deleted file mode 100644 index d7681b683..000000000 --- a/packages/wallet-widget/src/views/CollectionDetails/index.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { formatDisplay, ContractVerificationStatus } from '@0xsequence/connect' -import { Image, Text, TokenImage } from '@0xsequence/design-system' -import { useGetTokenBalancesDetails } from '@0xsequence/hooks' -import { TokenBalance } from '@0xsequence/indexer' -import { formatUnits } from 'viem' -import { useAccount } from 'wagmi' - -import { NetworkBadge } from '../../components/NetworkBadge' -import { useNavigation } from '../../hooks' - -import { CollectionDetailsSkeleton } from './Skeleton' - -interface CollectionDetailsProps { - chainId: number - contractAddress: string -} - -export const CollectionDetails = ({ chainId, contractAddress }: CollectionDetailsProps) => { - const { setNavigation } = useNavigation() - const { address: accountAddress } = useAccount() - const { data: collectionBalanceData, isPending: isPendingCollectionBalance } = useGetTokenBalancesDetails({ - chainIds: [chainId], - filter: { - accountAddresses: accountAddress ? [accountAddress] : [], - contractStatus: ContractVerificationStatus.ALL, - contractWhitelist: [contractAddress], - omitNativeBalances: true - } - }) - - const contractInfo = collectionBalanceData?.[0]?.contractInfo - const collectionLogoURI = contractInfo?.logoURI - - if (isPendingCollectionBalance) { - return - } - - const onClickItem = (balance: TokenBalance) => { - setNavigation({ - location: 'collectible-details', - params: { - contractAddress: balance.contractAddress, - chainId: balance.chainId, - tokenId: balance.tokenID || '' - } - }) - } - - return ( -
-
- - - {contractInfo?.name || 'Unknown'} - - - {`${ - collectionBalanceData?.length || 0 - } Unique Collectibles`} -
-
- - {`Owned (${collectionBalanceData?.length || 0})`} - -
- {collectionBalanceData?.map((balance, index) => { - const unformattedBalance = balance.balance - const decimals = balance?.tokenMetadata?.decimals || 0 - const formattedBalance = formatDisplay(formatUnits(BigInt(unformattedBalance), decimals)) - - return ( -
onClickItem(balance)}> -
- -
-
- - {`${balance.tokenMetadata?.name}`} - -
-
- - {formattedBalance} Owned - -
-
- ) - })} -
-
-
- ) -} diff --git a/packages/wallet-widget/src/views/History.tsx b/packages/wallet-widget/src/views/History.tsx index b3341c7d2..c7a40dbf4 100644 --- a/packages/wallet-widget/src/views/History.tsx +++ b/packages/wallet-widget/src/views/History.tsx @@ -1,28 +1,153 @@ +import { getNativeTokenInfoByChainId } from '@0xsequence/connect' +import { compareAddress, SearchIcon, TextInput } from '@0xsequence/design-system' import { useGetTransactionHistorySummary } from '@0xsequence/hooks' -import React from 'react' -import { useAccount } from 'wagmi' +import { Transaction } from '@0xsequence/indexer' +import Fuse from 'fuse.js' +import { useObservable } from 'micro-observables' +import { useMemo, useState } from 'react' +import { zeroAddress } from 'viem' +import { useConfig } from 'wagmi' +import { FilterButton } from '../components/Filter/FilterButton' import { TransactionHistoryList } from '../components/TransactionHistoryList' import { useSettings } from '../hooks' export const History = () => { - const { selectedNetworks } = useSettings() - const { address: accountAddress } = useAccount() + const { chains } = useConfig() + const { selectedNetworksObservable, selectedWalletsObservable } = useSettings() + + const selectedNetworks = useObservable(selectedNetworksObservable) + const selectedWallets = useObservable(selectedWalletsObservable) + + const [search, setSearch] = useState('') const { data: transactionHistory = [], isPending: isPendingTransactionHistory } = useGetTransactionHistorySummary({ - accountAddress: accountAddress || '', + accountAddresses: selectedWallets.map(wallet => wallet.address), chainIds: selectedNetworks }) + const fuseOptions = { + threshold: 0.1, + ignoreLocation: true, + keys: [ + { + name: 'contractName', + getFn: (transaction: Transaction) => { + return transaction.transfers?.map(transfer => transfer.contractInfo?.name).join(', ') || '' + } + }, + { + name: 'tokenSymbol', + getFn: (transaction: Transaction) => { + const hasNativeToken = transaction.transfers?.some(transfer => + compareAddress(transfer.contractInfo?.address, zeroAddress) + ) + if (hasNativeToken) { + const nativeTokenInfo = getNativeTokenInfoByChainId(transaction.chainId, chains) + return nativeTokenInfo.symbol + } + return transaction.transfers?.map(transfer => transfer.contractInfo?.symbol).join(', ') || '' + } + }, + { + name: 'collectibleName', + getFn: (transaction: Transaction) => { + return ( + transaction.transfers + ?.map(transfer => { + return Object.values(transfer.tokenMetadata || {}) + .map(tokenMetadata => { + return tokenMetadata.name + }) + .join(', ') + }) + .join(', ') || '' + ) + } + }, + { + name: 'date', + getFn: (transaction: Transaction) => { + const date = new Date(transaction.timestamp) + const day = date.getDate() + const month = date.toLocaleString('en-US', { month: 'long' }) + const year = date.getFullYear() + return ` + ${day} ${month} ${year} + ${day} ${year} ${month} + ${month} ${day} ${year} + ${month} ${year} ${day} + ${year} ${day} ${month} + ${year} ${month} ${day} + ` + }, + customSearchFn: (query: string, options: { keys: string[] }) => { + const queryParts = query.toLowerCase().split(/\s+/) + const dayQuery = queryParts.find(part => /^\d{1,2}$/.test(part)) + const monthQuery = queryParts.find(part => /^[a-zA-Z]+$/.test(part)) + const yearQuery = queryParts.find(part => /^\d{4}$/.test(part)) + + console.log(dayQuery, monthQuery, yearQuery) + + return options.keys.some(key => { + const keyParts = key.toLowerCase().split(/\s+/) + let keyDay = '', + keyMonth = '', + keyYear = '' + + keyParts.forEach(part => { + if (/^\d{1,2}$/.test(part)) { + keyDay = part + } else if (/^[a-zA-Z]+$/.test(part)) { + keyMonth = part + } else if (/^\d{4}$/.test(part)) { + keyYear = part + } + }) + + return ( + (!dayQuery || keyDay === dayQuery) && + (!monthQuery || keyMonth === monthQuery) && + (!yearQuery || keyYear === yearQuery) + ) + }) + } + } + ] + } + + const fuse = useMemo(() => { + return new Fuse(transactionHistory, fuseOptions) + }, [transactionHistory]) + + const searchResults = useMemo(() => { + if (!search.trimStart()) { + return [] + } + return fuse.search(search).map(result => result.item) + }, [search, fuse]) + return ( -
-
- +
+
+
+ setSearch(ev.target.value)} + placeholder="Search your wallet" + data-1p-ignore + /> +
+
+
) } diff --git a/packages/wallet-widget/src/views/Home/OperationButtonTemplate.tsx b/packages/wallet-widget/src/views/Home/OperationButtonTemplate.tsx new file mode 100644 index 000000000..6f88fa74b --- /dev/null +++ b/packages/wallet-widget/src/views/Home/OperationButtonTemplate.tsx @@ -0,0 +1,30 @@ +import { cardVariants, cn, Text, IconProps } from '@0xsequence/design-system' +import { ComponentType } from 'react' + +export const OperationButtonTemplate = ({ + label, + onClick, + icon: Icon +}: { + label: string + onClick: () => void + icon: ComponentType +}) => { + return ( +
+ {Icon && } + + {label} + +
+ ) +} diff --git a/packages/wallet-widget/src/views/Home/components/AssetSummary/CoinTile/CoinTileContent.tsx b/packages/wallet-widget/src/views/Home/components/AssetSummary/CoinTile/CoinTileContent.tsx deleted file mode 100644 index 9f280dbed..000000000 --- a/packages/wallet-widget/src/views/Home/components/AssetSummary/CoinTile/CoinTileContent.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { NetworkImage, Text, TokenImage } from '@0xsequence/design-system' -import React from 'react' - -import { useSettings } from '../../../../../hooks' -import { getPercentageColor } from '../../../../../utils' - -interface CoinTileContentProps { - logoUrl?: string - tokenName: string - balance: string - balanceFiat: string - priceChangePercentage: number - symbol: string - chainId: number -} - -export const CoinTileContent = ({ - logoUrl, - tokenName, - balance, - balanceFiat, - priceChangePercentage, - symbol, - chainId -}: CoinTileContentProps) => { - const { fiatCurrency } = useSettings() - const priceChangeSymbol = priceChangePercentage > 0 ? '+' : '' - - return ( -
-
- -
-
-
- - {tokenName} - - -
- - {`${balance} ${symbol}`} - -
-
-
- {`${fiatCurrency.sign}${balanceFiat}`} -
- - {`${priceChangeSymbol}${priceChangePercentage.toFixed(2)}%`} - -
-
- ) -} diff --git a/packages/wallet-widget/src/views/Home/components/AssetSummary/CoinTile/index.tsx b/packages/wallet-widget/src/views/Home/components/AssetSummary/CoinTile/index.tsx deleted file mode 100644 index 14c32d3be..000000000 --- a/packages/wallet-widget/src/views/Home/components/AssetSummary/CoinTile/index.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { compareAddress, formatDisplay, getNativeTokenInfoByChainId } from '@0xsequence/connect' -import { useGetContractInfo, useGetCoinPrices, useGetExchangeRate } from '@0xsequence/hooks' -import { TokenBalance } from '@0xsequence/indexer' -import { formatUnits, zeroAddress } from 'viem' -import { useConfig } from 'wagmi' - -import { useSettings } from '../../../../../hooks' -import { computeBalanceFiat, getPercentagePriceChange } from '../../../../../utils' - -import { CoinTileContent } from './CoinTileContent' - -interface CoinTileProps { - balance: TokenBalance -} - -export const CoinTile = ({ balance }: CoinTileProps) => { - const { chains } = useConfig() - const { fiatCurrency } = useSettings() - const isNativeToken = compareAddress(balance.contractAddress, zeroAddress) - const nativeTokenInfo = getNativeTokenInfoByChainId(balance.chainId, chains) - - const { data: dataCoinPrices = [], isPending: isPendingCoinPrice } = useGetCoinPrices([ - { - chainId: balance.chainId, - contractAddress: balance.contractAddress - } - ]) - - const { data: conversionRate = 1, isPending: isPendingConversionRate } = useGetExchangeRate(fiatCurrency.symbol) - - const { data: contractInfo, isPending: isPendingContractInfo } = useGetContractInfo({ - chainID: String(balance.chainId), - contractAddress: balance.contractAddress - }) - - const isPending = isPendingCoinPrice || isPendingConversionRate || isPendingContractInfo - if (isPending) { - return
- } - - if (isNativeToken) { - const computedBalance = computeBalanceFiat({ - balance, - prices: dataCoinPrices, - conversionRate, - decimals: nativeTokenInfo.decimals - }) - const priceChangePercentage = getPercentagePriceChange(balance, dataCoinPrices) - const formattedBalance = formatUnits(BigInt(balance.balance), nativeTokenInfo.decimals) - const balanceDisplayed = formatDisplay(formattedBalance) - - return ( - - ) - } - - const decimals = contractInfo?.decimals ?? 18 - - const computedBalance = computeBalanceFiat({ - balance, - prices: dataCoinPrices, - conversionRate, - decimals - }) - const priceChangePercentage = getPercentagePriceChange(balance, dataCoinPrices) - - const formattedBalance = formatUnits(BigInt(balance.balance), decimals) - const balanceDisplayed = formatDisplay(formattedBalance) - - const name = contractInfo?.name || 'Unknown' - const symbol = contractInfo?.name || 'TOKEN' - const url = contractInfo?.logoURI - - return ( - - ) -} diff --git a/packages/wallet-widget/src/views/Home/components/AssetSummary/CollectibleTile/index.tsx b/packages/wallet-widget/src/views/Home/components/AssetSummary/CollectibleTile/index.tsx deleted file mode 100644 index 7fbde1319..000000000 --- a/packages/wallet-widget/src/views/Home/components/AssetSummary/CollectibleTile/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useGetTokenMetadata } from '@0xsequence/hooks' -import { TokenBalance } from '@0xsequence/indexer' - -import { CollectibleTileImage } from '../../../../../components/CollectibleTileImage' - -interface CollectibleTileProps { - balance: TokenBalance -} - -export const CollectibleTile = ({ balance }: CollectibleTileProps) => { - const { data: tokenMetadata } = useGetTokenMetadata({ - chainID: String(balance.chainId), - contractAddress: balance.contractAddress, - tokenIDs: [balance.tokenID || ''] - }) - - const imageUrl = tokenMetadata?.[0]?.image - - return -} diff --git a/packages/wallet-widget/src/views/Home/components/AssetSummary/SkeletonTiles.tsx b/packages/wallet-widget/src/views/Home/components/AssetSummary/SkeletonTiles.tsx deleted file mode 100644 index 5c945d3bd..000000000 --- a/packages/wallet-widget/src/views/Home/components/AssetSummary/SkeletonTiles.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Skeleton } from '@0xsequence/design-system' -import React from 'react' - -export const SkeletonTiles = () => { - return ( -
- {Array(12) - .fill(null) - .map((_, i) => ( -
- -
- ))} -
- ) -} diff --git a/packages/wallet-widget/src/views/Home/components/AssetSummary/index.tsx b/packages/wallet-widget/src/views/Home/components/AssetSummary/index.tsx deleted file mode 100644 index c8c066e05..000000000 --- a/packages/wallet-widget/src/views/Home/components/AssetSummary/index.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useWalletSettings } from '@0xsequence/connect' -import { Spinner } from '@0xsequence/design-system' -import { useGetTokenBalancesDetails } from '@0xsequence/hooks' -import { ContractVerificationStatus, TokenBalance } from '@0xsequence/indexer' -import { useEffect, useRef, useState } from 'react' -import { useAccount } from 'wagmi' - -import { useNavigation, useSettings } from '../../../../hooks' - -import { CoinTile } from './CoinTile' -import { CollectibleTile } from './CollectibleTile' -import { SkeletonTiles } from './SkeletonTiles' - -export const AssetSummary = () => { - const { address } = useAccount() - const { setNavigation } = useNavigation() - const { displayedAssets } = useWalletSettings() - const { hideUnlistedTokens, hideCollectibles, selectedNetworks } = useSettings() - - const pageSize = 10 - const [isLoading, setIsLoading] = useState(false) - const [displayedTokens, setDisplayedTokens] = useState([]) - const [hasMoreTokens, setHasMoreTokens] = useState(false) - - const endOfPageRef = useRef(null) - - const fetchMoreTokens = () => { - if (displayedTokens.length >= balances.length) { - setHasMoreTokens(false) - return - } - setDisplayedTokens(balances.slice(0, displayedTokens.length + pageSize)) - } - - useEffect(() => { - if (!endOfPageRef.current) { - return - } - - const observer = new IntersectionObserver(entries => { - const endOfPage = entries[0] - if (endOfPage.isIntersecting && hasMoreTokens) { - setIsLoading(true) - setTimeout(() => { - fetchMoreTokens() - setIsLoading(false) - }, 500) - } - }) - - observer.observe(endOfPageRef.current) - - return () => { - observer.disconnect() - } - }, [hasMoreTokens, fetchMoreTokens]) - - const { data: balances = [], isPending: isPendingBalances } = useGetTokenBalancesDetails( - { - filter: { - accountAddresses: [address || ''], - contractWhitelist: displayedAssets.map(asset => asset.contractAddress), - contractStatus: hideUnlistedTokens ? ContractVerificationStatus.VERIFIED : ContractVerificationStatus.ALL, - omitNativeBalances: false - }, - chainIds: selectedNetworks - }, - { hideCollectibles } - ) - - useEffect(() => { - if (!isPendingBalances && balances.length > 0) { - setDisplayedTokens(balances.slice(0, pageSize)) - setHasMoreTokens(balances.length > pageSize) - } - // only runs once after balances are fetched - }, [balances, isPendingBalances]) - - if (isPendingBalances) { - return - } - - const onClickItem = (balance: TokenBalance) => { - if (balance.contractType === 'ERC1155' || balance.contractType === 'ERC721') { - setNavigation({ - location: 'collectible-details', - params: { - contractAddress: balance.contractAddress, - chainId: balance.chainId, - tokenId: balance.tokenID || '' - } - }) - } else { - setNavigation({ - location: 'coin-details', - params: { - contractAddress: balance.contractAddress, - chainId: balance.chainId - } - }) - } - } - - return ( -
-
- {displayedTokens.map((balance, index) => { - return ( -
onClickItem(balance)}> - {balance.contractType === 'ERC1155' || balance.contractType === 'ERC721' ? ( - - ) : ( - - )} -
- ) - })} -
- {isLoading && } -
-
- ) -} diff --git a/packages/wallet-widget/src/views/Home/index.tsx b/packages/wallet-widget/src/views/Home/index.tsx index c7001e029..3feb1bf62 100644 --- a/packages/wallet-widget/src/views/Home/index.tsx +++ b/packages/wallet-widget/src/views/Home/index.tsx @@ -1,11 +1,389 @@ -import React from 'react' +import { useAddFundsModal } from '@0xsequence/checkout' +import { compareAddress, formatAddress, useWallets, useOpenConnectModal, getNativeTokenInfoByChainId } from '@0xsequence/connect' +import { + Button, + ArrowUpIcon, + SwapIcon, + ScanIcon, + AddIcon, + ChevronUpDownIcon, + Text, + EllipsisIcon, + Skeleton +} from '@0xsequence/design-system' +import { useGetCoinPrices, useGetExchangeRate, useGetTokenBalancesDetails } from '@0xsequence/hooks' +import { ContractVerificationStatus } from '@0xsequence/indexer' +import { ethers } from 'ethers' +import { useObservable } from 'micro-observables' +import { AnimatePresence } from 'motion/react' +import { useEffect, useMemo, useState } from 'react' +import { useAccount, useConfig } from 'wagmi' -import { AssetSummary } from './components/AssetSummary' +import { CopyButton } from '../../components/CopyButton' +import { WalletsFilter } from '../../components/Filter/WalletsFilter' +import { StackedIconTag } from '../../components/IconWrappers/StackedIconTag' +import { ListCardNav } from '../../components/ListCard/ListCardNav' +import { ListCardNavTable } from '../../components/ListCardTable/ListCardNavTable' +import { SelectWalletRow } from '../../components/Select/SelectWalletRow' +import { SlideupDrawer } from '../../components/Select/SlideupDrawer' +import { WalletAccountGradient } from '../../components/WalletAccountGradient' +import { useNavigation, useSettings } from '../../hooks' +import { useFiatWalletsMap } from '../../hooks/useFiatWalletsMap' +import { computeBalanceFiat } from '../../utils' + +import { OperationButtonTemplate } from './OperationButtonTemplate' export const Home = () => { + const { setNavigation } = useNavigation() + const { selectedWalletsObservable, selectedNetworks, hideUnlistedTokens, fiatCurrency, selectedCollections } = useSettings() + const { fiatWalletsMap } = useFiatWalletsMap() + const { connector } = useAccount() + + const selectedWallets = useObservable(selectedWalletsObservable) + const { chains } = useConfig() + const { address: accountAddress } = useAccount() + const { wallets, setActiveWallet } = useWallets() + + const { setOpenConnectModal } = useOpenConnectModal() + const { triggerAddFunds } = useAddFundsModal() + const [accountSelectorModalOpen, setAccountSelectorModalOpen] = useState(false) + const [walletFilterOpen, setWalletFilterOpen] = useState(false) + + const [signInDisplay, setSignInDisplay] = useState('') + + useEffect(() => { + const fetchSignInDisplay = async () => { + const sequenceWaas = (await connector?.sequenceWaas) as { + listAccounts: () => Promise<{ accounts: { email: string; type: string }[] }> + } + + if (sequenceWaas) { + const sequenceWaasAccounts = await sequenceWaas.listAccounts() + const waasEmail = sequenceWaasAccounts.accounts.find(account => account.type === 'OIDC')?.email + let backupEmail = '' + if (sequenceWaasAccounts.accounts.length > 0) { + backupEmail = sequenceWaasAccounts.accounts[0].email + } + setSignInDisplay(waasEmail || backupEmail) + } else { + setSignInDisplay(connector?.name || '') + } + } + fetchSignInDisplay() + }, [connector]) + + const { data: tokenBalancesData, isPending: isTokenBalancesPending } = useGetTokenBalancesDetails({ + chainIds: selectedNetworks, + filter: { + accountAddresses: selectedWallets.map(wallet => wallet.address), + contractStatus: hideUnlistedTokens ? ContractVerificationStatus.VERIFIED : ContractVerificationStatus.ALL, + contractWhitelist: selectedCollections.map(collection => collection.contractAddress), + omitNativeBalances: false + } + }) + + const coinBalancesUnordered = + tokenBalancesData?.filter(b => b.contractType === 'ERC20' || compareAddress(b.contractAddress, ethers.ZeroAddress)) || [] + + const isSingleCollectionSelected = selectedCollections.length === 1 + + const collectibleBalancesUnordered = + tokenBalancesData?.filter(token => { + if (token.contractType !== 'ERC721' && token.contractType !== 'ERC1155') { + return false + } + if (isSingleCollectionSelected) { + return token.chainId === selectedCollections[0].chainId + } + return true + }) || [] + + const { data: coinPrices = [], isPending: isCoinPricesPending } = useGetCoinPrices( + coinBalancesUnordered.map(token => ({ + chainId: token.chainId, + contractAddress: token.contractAddress + })) + ) + + const { data: conversionRate, isPending: isConversionRatePending } = useGetExchangeRate(fiatCurrency.symbol) + + const isPending = isTokenBalancesPending || isCoinPricesPending || isConversionRatePending + + const totalFiatValue = fiatWalletsMap + .reduce((acc, wallet) => { + if (selectedWallets.some(selectedWallet => selectedWallet.address === wallet.accountAddress)) { + const walletFiatValue = Number(wallet.fiatValue) + return acc + walletFiatValue + } + return acc + }, 0) + .toFixed(2) + + const coinBalances = coinBalancesUnordered.sort((a, b) => { + const isHigherFiat = + Number( + computeBalanceFiat({ + balance: b, + prices: coinPrices, + conversionRate: conversionRate || 1, + decimals: b.contractInfo?.decimals || 18 + }) + ) - + Number( + computeBalanceFiat({ + balance: a, + prices: coinPrices, + conversionRate: conversionRate || 1, + decimals: a.contractInfo?.decimals || 18 + }) + ) + return isHigherFiat + }) + + const collectibleBalances = collectibleBalancesUnordered.sort((a, b) => { + return Number(b.balance) - Number(a.balance) + }) + + const coinBalancesIconSet = new Set() + const coinBalancesIcons = useMemo( + () => + coinBalances + .map(coin => { + const isNativeToken = compareAddress(coin.contractAddress, ethers.ZeroAddress) + const nativeTokenInfo = getNativeTokenInfoByChainId(coin.chainId, chains) + const logoURI = isNativeToken ? nativeTokenInfo.logoURI : coin.contractInfo?.logoURI + const tokenName = isNativeToken ? nativeTokenInfo.name : coin.contractInfo?.name + + if (coinBalancesIconSet.has(tokenName) || !logoURI) { + return + } + + coinBalancesIconSet.add(tokenName) + if (coinBalancesIconSet.size <= 3) { + return logoURI + } + }) + .filter(Boolean) as string[], + [coinBalances, selectedWallets, selectedNetworks, hideUnlistedTokens, selectedCollections] + ) + + const collectibleBalancesIconSet = new Set() + const collectibleBalancesIcons = useMemo( + () => + collectibleBalances + .map(collectible => { + const logoURI = collectible.tokenMetadata?.image + + if (collectibleBalancesIconSet.has(logoURI) || !logoURI) { + return + } + + collectibleBalancesIconSet.add(logoURI) + if (collectibleBalancesIconSet.size <= 3) { + return logoURI + } + }) + .filter(Boolean) as string[], + [collectibleBalances, selectedWallets, selectedNetworks, hideUnlistedTokens, selectedCollections] + ) + + const onClickAccountSelector = () => { + setAccountSelectorModalOpen(true) + } + const handleAddNewWallet = () => { + setAccountSelectorModalOpen(false) + setOpenConnectModal(true) + } + const onClickSend = () => { + setNavigation({ + location: 'send-general' + }) + } + const onClickSwap = () => { + setNavigation({ + location: 'swap' + }) + } + const onClickReceive = () => { + setNavigation({ + location: 'receive' + }) + } + const onClickAddFunds = () => { + triggerAddFunds({ walletAddress: accountAddress || '' }) + } + const onClickWalletSelector = () => { + setWalletFilterOpen(true) + } + const onClickTokens = () => { + setNavigation({ + location: 'search-tokens' + }) + } + const onClickCollectibles = () => { + setNavigation({ + location: 'search-collectibles' + }) + } + const onClickTransactions = () => { + setNavigation({ + location: 'history' + }) + } + const onClickSettings = () => { + setNavigation({ + location: 'settings' + }) + } + + const homeNavTableItems = [ + 0 ? ( +
+ + {fiatCurrency.sign} + {isPending ? : `${totalFiatValue}`} + + + {coinBalances.length} + + } + /> +
+ ) : ( + + No Tokens + + ) + } + shape="square" + > + + Tokens + +
, + 0 ? ( + + {collectibleBalances.length} + + } + shape="square" + /> + ) : ( + + No Collectibles + + ) + } + shape="square" + > + + Collectibles + + + ] + return ( -
- +
+
+ +
+
+ + {formatAddress(accountAddress || '')} + + +
+ {signInDisplay && ( + + {signInDisplay} + + )} +
+ +
+
+ + + + +
+
+ + <> + + Items + + wallet.address)} + label={ +
+ + {`${selectedWallets.length} Wallet${selectedWallets.length === 1 ? '' : 's'}`} + + +
+ } + isAccount + enabled + onClick={onClickWalletSelector} + /> + +
+ + + Transactions + + + + + Settings + + +
+ + + {accountSelectorModalOpen && ( + setAccountSelectorModalOpen(false)} + label="Select main wallet" + buttonLabel="+ Add new wallet" + handleButtonPress={handleAddNewWallet} + dragHandleWidth={74} + > +
+ {wallets.map((wallet, index) => ( + setAccountSelectorModalOpen(false)} + onClick={setActiveWallet} + /> + ))} +
+
+ )} + {walletFilterOpen && ( + setWalletFilterOpen(false)} label="Select active wallet"> + + + )} +
) } diff --git a/packages/wallet-widget/src/views/Receive.tsx b/packages/wallet-widget/src/views/Receive.tsx index f51c85d94..231ec33f0 100644 --- a/packages/wallet-widget/src/views/Receive.tsx +++ b/packages/wallet-widget/src/views/Receive.tsx @@ -1,18 +1,21 @@ -import { getNativeTokenInfoByChainId } from '@0xsequence/connect' +import { getNetwork } from '@0xsequence/connect' import { Button, Text, CopyIcon, ShareIcon, Image } from '@0xsequence/design-system' import { QRCodeCanvas } from 'qrcode.react' -import React, { useState, useEffect } from 'react' +import { useState, useEffect } from 'react' import { CopyToClipboard } from 'react-copy-to-clipboard' -import { useAccount, useConfig } from 'wagmi' +import { useAccount } from 'wagmi' -import { HEADER_HEIGHT } from '../constants' +import { NetworkSelect } from '../components/Select/NetworkSelect' +import { HEADER_HEIGHT_WITH_LABEL } from '../constants' + +const isVowel = (char: string) => ['a', 'e', 'i', 'o', 'u'].includes(char.toLowerCase()) export const Receive = () => { const { address, chain } = useAccount() - const { chains } = useConfig() const [isCopied, setCopied] = useState(false) - const nativeTokenInfo = getNativeTokenInfoByChainId(chain?.id || 1, chains) + const networkInfo = getNetwork(chain?.id || 1) + const networkName = networkInfo.title || networkInfo.name useEffect(() => { if (isCopied) { @@ -33,8 +36,9 @@ export const Receive = () => { } return ( -
-
+
+
+
@@ -43,7 +47,7 @@ export const Receive = () => { My Wallet - icon + icon
{ overflowWrap: 'anywhere' }} > - {`This is a ${nativeTokenInfo.name} address. Please only send assets on the ${nativeTokenInfo.name} network.`} + {`This is a${isVowel(networkName[0]) ? 'n' : ''} ${networkName} address. Please only send assets on the ${networkName} network.`}
diff --git a/packages/wallet-widget/src/views/Search/SearchCollectibles.tsx b/packages/wallet-widget/src/views/Search/SearchCollectibles.tsx new file mode 100644 index 000000000..689168d47 --- /dev/null +++ b/packages/wallet-widget/src/views/Search/SearchCollectibles.tsx @@ -0,0 +1,59 @@ +import { ContractVerificationStatus } from '@0xsequence/connect' +import { useGetTokenBalancesDetails } from '@0xsequence/hooks' +import { useObservable } from 'micro-observables' + +import { CollectiblesList } from '../../components/SearchLists/CollectiblesList' +import { useSettings, useNavigation } from '../../hooks' +import { TokenBalanceWithPrice } from '../../utils' + +export const SearchCollectibles = () => { + const { selectedWalletsObservable, selectedNetworksObservable, selectedCollectionsObservable, hideUnlistedTokens } = + useSettings() + const { setNavigation } = useNavigation() + + const selectedWallets = useObservable(selectedWalletsObservable) + const selectedNetworks = useObservable(selectedNetworksObservable) + const selectedCollections = useObservable(selectedCollectionsObservable) + + const { data: tokenBalancesData = [], isPending: isPendingTokenBalances } = useGetTokenBalancesDetails({ + chainIds: selectedNetworks, + filter: { + accountAddresses: selectedWallets.map(wallet => wallet.address), + contractStatus: hideUnlistedTokens ? ContractVerificationStatus.VERIFIED : ContractVerificationStatus.ALL, + contractWhitelist: selectedCollections.map(collection => collection.contractAddress), + omitNativeBalances: false + } + }) + + const isSingleCollectionSelected = selectedCollections.length === 1 + + const collectibleBalancesFiltered = tokenBalancesData.filter(token => { + if (isSingleCollectionSelected) { + return token.chainId === selectedCollections[0].chainId + } + return true + }) + + const onHandleCollectibleClick = (balance: TokenBalanceWithPrice) => { + setNavigation({ + location: 'collectible-details', + params: { + contractAddress: balance.contractAddress, + chainId: balance.chainId, + tokenId: balance.tokenID || '', + accountAddress: balance.accountAddress + } + }) + } + + return ( +
+ +
+ ) +} diff --git a/packages/wallet-widget/src/views/Search/SearchTokens.tsx b/packages/wallet-widget/src/views/Search/SearchTokens.tsx new file mode 100644 index 000000000..576aaf1d2 --- /dev/null +++ b/packages/wallet-widget/src/views/Search/SearchTokens.tsx @@ -0,0 +1,46 @@ +import { useGetTokenBalancesSummary } from '@0xsequence/hooks' +import { ContractVerificationStatus } from '@0xsequence/indexer' +import { useObservable } from 'micro-observables' + +import { TokenList } from '../../components/SearchLists' +import { useNavigation, useSettings } from '../../hooks' +import { TokenBalanceWithPrice } from '../../utils' + +export const SearchTokens = () => { + const { setNavigation } = useNavigation() + const { selectedWalletsObservable, selectedNetworksObservable, hideUnlistedTokens } = useSettings() + + const selectedWallets = useObservable(selectedWalletsObservable) + const selectedNetworks = useObservable(selectedNetworksObservable) + + const { data: tokenBalancesData = [], isPending: isPendingTokenBalances } = useGetTokenBalancesSummary({ + chainIds: selectedNetworks, + filter: { + accountAddresses: selectedWallets.map(wallet => wallet.address), + contractStatus: hideUnlistedTokens ? ContractVerificationStatus.VERIFIED : ContractVerificationStatus.ALL, + omitNativeBalances: false + } + }) + + const handleTokenClick = (balance: TokenBalanceWithPrice) => { + setNavigation({ + location: 'coin-details', + params: { + contractAddress: balance.contractAddress, + chainId: balance.chainId, + accountAddress: balance.accountAddress + } + }) + } + + return ( +
+ +
+ ) +} diff --git a/packages/wallet-widget/src/views/Search/SearchWallet.tsx b/packages/wallet-widget/src/views/Search/SearchWallet.tsx deleted file mode 100644 index fb45e1caf..000000000 --- a/packages/wallet-widget/src/views/Search/SearchWallet.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { getNativeTokenInfoByChainId, ContractVerificationStatus, compareAddress } from '@0xsequence/connect' -import { SearchIcon, Skeleton, Text, TextInput } from '@0xsequence/design-system' -import { useGetTokenBalancesSummary, useGetCoinPrices, useGetExchangeRate } from '@0xsequence/hooks' -import Fuse from 'fuse.js' -import { useState } from 'react' -import { zeroAddress } from 'viem' -import { useAccount, useConfig } from 'wagmi' - -import { useSettings } from '../../hooks' -import { computeBalanceFiat } from '../../utils' - -import { BalanceItem } from './components/BalanceItem' -import { WalletLink } from './components/WalletLink' - -export const SearchWallet = () => { - const { chains } = useConfig() - const { fiatCurrency, hideUnlistedTokens, selectedNetworks } = useSettings() - const [search, setSearch] = useState('') - const { address: accountAddress } = useAccount() - - const { data: tokenBalancesData, isPending: isPendingTokenBalances } = useGetTokenBalancesSummary({ - chainIds: selectedNetworks, - filter: { - accountAddresses: accountAddress ? [accountAddress] : [], - contractStatus: hideUnlistedTokens ? ContractVerificationStatus.VERIFIED : ContractVerificationStatus.ALL, - omitNativeBalances: false - } - }) - - const coinBalancesUnordered = - tokenBalancesData?.filter(b => b.contractType === 'ERC20' || compareAddress(b.contractAddress, zeroAddress)) || [] - - const { data: coinPrices = [], isPending: isPendingCoinPrices } = useGetCoinPrices( - coinBalancesUnordered.map(token => ({ - chainId: token.chainId, - contractAddress: token.contractAddress - })) - ) - - const { data: conversionRate = 1, isPending: isPendingConversionRate } = useGetExchangeRate(fiatCurrency.symbol) - - const coinBalances = coinBalancesUnordered.sort((a, b) => { - const isHigherFiat = - Number( - computeBalanceFiat({ - balance: b, - prices: coinPrices, - conversionRate, - decimals: b.contractInfo?.decimals || 18 - }) - ) - - Number( - computeBalanceFiat({ - balance: a, - prices: coinPrices, - conversionRate, - decimals: a.contractInfo?.decimals || 18 - }) - ) - return isHigherFiat - }) - - const collectionBalancesUnordered = - tokenBalancesData?.filter(b => b.contractType === 'ERC721' || b.contractType === 'ERC1155') || [] - const collectionBalances = collectionBalancesUnordered.sort((a, b) => { - return Number(b.balance) - Number(a.balance) - }) - - const isPending = isPendingTokenBalances || isPendingCoinPrices || isPendingConversionRate - - interface IndexedData { - index: number - name: string - } - const indexedCollectionBalances: IndexedData[] = collectionBalances.map((balance, index) => { - return { - index, - name: balance.contractInfo?.name || 'Unknown' - } - }) - - const indexedCoinBalances: IndexedData[] = coinBalances.map((balance, index) => { - if (compareAddress(balance.contractAddress, zeroAddress)) { - const nativeTokenInfo = getNativeTokenInfoByChainId(balance.chainId, chains) - - return { - index, - name: nativeTokenInfo.name - } - } else { - return { - index, - name: balance.contractInfo?.name || 'Unknown' - } - } - }) - - const coinBalancesAmount = coinBalances.length - const collectionBalancesAmount = collectionBalances.length - - const fuzzySearchCoinBalances = new Fuse(indexedCoinBalances, { - keys: ['name'] - }) - - const fuzzySearchCollections = new Fuse(indexedCollectionBalances, { - keys: ['name'] - }) - - const foundCoinBalances = ( - search === '' ? indexedCoinBalances : fuzzySearchCoinBalances.search(search).map(result => result.item) - ).slice(0, 5) - const foundCollectionBalances = ( - search === '' ? indexedCollectionBalances : fuzzySearchCollections.search(search).map(result => result.item) - ).slice(0, 5) - - return ( -
-
- setSearch(ev.target.value)} - placeholder="Search your wallet" - data-1p-ignore - /> -
-
- - {isPending ? ( - Array(5) - .fill(null) - .map((_, i) => ) - ) : foundCoinBalances.length === 0 ? ( - No coins found - ) : ( - foundCoinBalances.map((indexItem, index) => { - const balance = coinBalances[indexItem.index] - return - }) - )} -
-
- - {isPending ? ( - Array(5) - .fill(null) - .map((_, i) => ) - ) : foundCollectionBalances.length === 0 ? ( - No collections found - ) : ( - foundCollectionBalances.map((indexedItem, index) => { - const balance = collectionBalances[indexedItem.index] - return - }) - )} -
-
- ) -} diff --git a/packages/wallet-widget/src/views/Search/SearchWalletViewAll.tsx b/packages/wallet-widget/src/views/Search/SearchWalletViewAll.tsx deleted file mode 100644 index cbce5852e..000000000 --- a/packages/wallet-widget/src/views/Search/SearchWalletViewAll.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import { getNativeTokenInfoByChainId, ContractVerificationStatus, compareAddress } from '@0xsequence/connect' -import { SearchIcon, Skeleton, TabsContent, TabsHeader, TabsRoot, TextInput } from '@0xsequence/design-system' -import { useGetTokenBalancesSummary, useGetCoinPrices, useGetExchangeRate } from '@0xsequence/hooks' -import Fuse from 'fuse.js' -import { useState, useEffect } from 'react' -import { zeroAddress } from 'viem' -import { useAccount, useConfig } from 'wagmi' - -import { useSettings } from '../../hooks' -import { computeBalanceFiat } from '../../utils' - -import { CoinsTab } from './components/CoinsTab' -import { CollectionsTab } from './components/CollectionsTab' - -interface SearchWalletViewAllProps { - defaultTab: 'coins' | 'collections' -} - -export interface IndexedData { - index: number - name: string -} - -export const SearchWalletViewAll = ({ defaultTab }: SearchWalletViewAllProps) => { - const { chains } = useConfig() - const { fiatCurrency, hideUnlistedTokens, selectedNetworks } = useSettings() - const [search, setSearch] = useState('') - const [selectedTab, setSelectedTab] = useState(defaultTab) - - const pageSize = 20 - const [displayedCoinBalances, setDisplayedCoinBalances] = useState([]) - const [displayedCollectionBalances, setDisplayedCollectionBalances] = useState([]) - - const [displayedSearchCoinBalances, setDisplayedSearchCoinBalances] = useState([]) - const [displayedSearchCollectionBalances, setDisplayedSearchCollectionBalances] = useState([]) - - const [initCoinsFlag, setInitCoinsFlag] = useState(false) - const [initCollectionsFlag, setInitCollectionsFlag] = useState(false) - - const [hasMoreCoins, sethasMoreCoins] = useState(false) - const [hasMoreCollections, sethasMoreCollections] = useState(false) - - const [hasMoreSearchCoins, sethasMoreSearchCoins] = useState(false) - const [hasMoreSearchCollections, sethasMoreSearchCollections] = useState(false) - - const { address: accountAddress } = useAccount() - - const { data: tokenBalancesData, isPending: isPendingTokenBalances } = useGetTokenBalancesSummary({ - chainIds: selectedNetworks, - filter: { - accountAddresses: accountAddress ? [accountAddress] : [], - contractStatus: hideUnlistedTokens ? ContractVerificationStatus.VERIFIED : ContractVerificationStatus.ALL, - omitNativeBalances: false - } - }) - - const coinBalancesUnordered = - tokenBalancesData?.filter(b => b.contractType === 'ERC20' || compareAddress(b.contractAddress, zeroAddress)) || [] - - const { data: coinPrices = [], isPending: isPendingCoinPrices } = useGetCoinPrices( - coinBalancesUnordered.map(token => ({ - chainId: token.chainId, - contractAddress: token.contractAddress - })) - ) - - const { data: conversionRate = 1, isPending: isPendingConversionRate } = useGetExchangeRate(fiatCurrency.symbol) - - const coinBalances = coinBalancesUnordered.sort((a, b) => { - const fiatA = computeBalanceFiat({ - balance: a, - prices: coinPrices, - conversionRate, - decimals: a.contractInfo?.decimals || 18 - }) - const fiatB = computeBalanceFiat({ - balance: b, - prices: coinPrices, - conversionRate, - decimals: b.contractInfo?.decimals || 18 - }) - return Number(fiatB) - Number(fiatA) - }) - - const collectionBalancesUnordered = - tokenBalancesData?.filter(b => b.contractType === 'ERC721' || b.contractType === 'ERC1155') || [] - - const collectionBalances = collectionBalancesUnordered.sort((a, b) => { - return Number(b.balance) - Number(a.balance) - }) - - const coinBalancesAmount = coinBalances.length - const collectionBalancesAmount = collectionBalances.length - - const isPending = isPendingTokenBalances || isPendingCoinPrices || isPendingConversionRate - - const indexedCoinBalances: IndexedData[] = coinBalances.map((balance, index) => { - if (compareAddress(balance.contractAddress, zeroAddress)) { - const nativeTokenInfo = getNativeTokenInfoByChainId(balance.chainId, chains) - - return { - index, - name: nativeTokenInfo.name - } - } else { - return { - index, - name: balance.contractInfo?.name || 'Unknown' - } - } - }) - - const indexedCollectionBalances: IndexedData[] = collectionBalances.map((balance, index) => ({ - index, - name: balance.contractInfo?.name || 'Unknown' - })) - - useEffect(() => { - if (!initCoinsFlag && indexedCoinBalances.length > 0) { - setDisplayedCoinBalances(indexedCoinBalances.slice(0, pageSize)) - sethasMoreCoins(indexedCoinBalances.length > pageSize) - setInitCoinsFlag(true) - } - }, [initCoinsFlag]) - - useEffect(() => { - if (!initCollectionsFlag && indexedCollectionBalances.length > 0) { - setDisplayedCollectionBalances(indexedCollectionBalances.slice(0, pageSize)) - sethasMoreCollections(indexedCollectionBalances.length > pageSize) - setInitCollectionsFlag(true) - } - }, [initCollectionsFlag]) - - useEffect(() => { - if (search !== '') { - setDisplayedSearchCoinBalances( - fuzzySearchCoinBalances - .search(search) - .map(result => result.item) - .slice(0, pageSize) - ) - sethasMoreSearchCoins(fuzzySearchCoinBalances.search(search).length > pageSize) - } - }, [search]) - - useEffect(() => { - if (search !== '') { - setDisplayedSearchCollectionBalances( - fuzzySearchCollections - .search(search) - .map(result => result.item) - .slice(0, pageSize) - ) - sethasMoreSearchCollections(fuzzySearchCollections.search(search).length > pageSize) - } - }, [search]) - - const fetchMoreCoinBalances = () => { - if (displayedCoinBalances.length >= indexedCoinBalances.length) { - sethasMoreCoins(false) - return - } - setDisplayedCoinBalances(indexedCoinBalances.slice(0, displayedCoinBalances.length + pageSize)) - } - - const fetchMoreCollectionBalances = () => { - if (displayedCollectionBalances.length >= indexedCollectionBalances.length) { - sethasMoreCollections(false) - return - } - setDisplayedCollectionBalances(indexedCollectionBalances.slice(0, displayedCollectionBalances.length + pageSize)) - } - - const fetchMoreSearchCoinBalances = () => { - if (displayedSearchCoinBalances.length >= fuzzySearchCoinBalances.search(search).length) { - sethasMoreSearchCoins(false) - return - } - setDisplayedSearchCoinBalances( - fuzzySearchCoinBalances - .search(search) - .map(result => result.item) - .slice(0, displayedSearchCoinBalances.length + pageSize) - ) - } - - const fetchMoreSearchCollectionBalances = () => { - if (displayedSearchCollectionBalances.length >= fuzzySearchCollections.search(search).length) { - sethasMoreSearchCollections(false) - return - } - setDisplayedSearchCollectionBalances( - fuzzySearchCollections - .search(search) - .map(result => result.item) - .slice(0, displayedSearchCollectionBalances.length + pageSize) - ) - } - - const fuzzySearchCoinBalances = new Fuse(indexedCoinBalances, { - keys: ['name'] - }) - - const fuzzySearchCollections = new Fuse(indexedCollectionBalances, { - keys: ['name'] - }) - - return ( -
-
- setSearch(ev.target.value)} - placeholder="Search your wallet" - data-1p-ignore - /> -
-
- setSelectedTab(value as 'coins' | 'collections')}> -
- {!isPending && ( - - )} - {isPending && } -
- - - - - - - - -
-
-
- ) -} diff --git a/packages/wallet-widget/src/views/Search/components/BalanceItem.tsx b/packages/wallet-widget/src/views/Search/components/BalanceItem.tsx deleted file mode 100644 index e2f49d4bc..000000000 --- a/packages/wallet-widget/src/views/Search/components/BalanceItem.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { compareAddress, formatDisplay, getNativeTokenInfoByChainId } from '@0xsequence/connect' -import { Text, ChevronRightIcon, TokenImage } from '@0xsequence/design-system' -import { TokenBalance } from '@0xsequence/indexer' -import React from 'react' -import { formatUnits, zeroAddress } from 'viem' -import { useConfig } from 'wagmi' - -import { useNavigation } from '../../../hooks' - -interface BalanceItemProps { - balance: TokenBalance -} - -export const BalanceItem = ({ balance }: BalanceItemProps) => { - const { chains } = useConfig() - const { setNavigation } = useNavigation() - const isNativeToken = compareAddress(balance.contractAddress, zeroAddress) - const nativeTokenInfo = getNativeTokenInfoByChainId(balance.chainId, chains) - const logoURI = isNativeToken ? nativeTokenInfo.logoURI : balance?.contractInfo?.logoURI - const tokenName = isNativeToken ? nativeTokenInfo.name : balance?.contractInfo?.name || 'Unknown' - const symbol = isNativeToken ? nativeTokenInfo.symbol : balance?.contractInfo?.symbol - - const getQuantity = () => { - if (balance.contractType === 'ERC721' || balance.contractType === 'ERC1155') { - return balance.uniqueCollectibles - } - const decimals = isNativeToken ? nativeTokenInfo.decimals : balance?.contractInfo?.decimals - const bal = formatUnits(BigInt(balance.balance), decimals || 0) - const displayBalance = formatDisplay(bal) - const symbol = isNativeToken ? nativeTokenInfo.symbol : balance?.contractInfo?.symbol - - return `${displayBalance} ${symbol}` - } - - const onClick = () => { - const isCollection = balance.contractType === 'ERC721' || balance.contractType === 'ERC1155' - if (isCollection) { - setNavigation({ - location: 'collection-details', - params: { - contractAddress: balance.contractAddress, - chainId: balance.chainId - } - }) - } else { - setNavigation({ - location: 'coin-details', - params: { - contractAddress: balance.contractAddress, - chainId: balance.chainId - } - }) - } - } - - return ( -
-
- - - {tokenName} - -
-
- - {getQuantity()} - - -
-
- ) -} diff --git a/packages/wallet-widget/src/views/Search/components/CoinsTab.tsx b/packages/wallet-widget/src/views/Search/components/CoinsTab.tsx deleted file mode 100644 index 9b60de348..000000000 --- a/packages/wallet-widget/src/views/Search/components/CoinsTab.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Spinner, Skeleton, Text } from '@0xsequence/design-system' -import { TokenBalance } from '@0xsequence/indexer' -import React, { useEffect, useRef, useState } from 'react' - -import { IndexedData } from '../SearchWalletViewAll' - -import { BalanceItem } from './BalanceItem' - -interface CoinsTabProps { - displayedCoinBalances: IndexedData[] - fetchMoreCoinBalances: () => void - fetchMoreSearchCoinBalances: () => void - hasMoreCoins: boolean - hasMoreSearchCoins: boolean - isSearching: boolean - isPending: boolean - coinBalances: TokenBalance[] -} - -export const CoinsTab: React.FC = ({ - displayedCoinBalances, - fetchMoreCoinBalances, - fetchMoreSearchCoinBalances, - hasMoreCoins, - hasMoreSearchCoins, - isSearching, - isPending, - coinBalances -}) => { - const [isLoading, setIsLoading] = useState(false) - - const endOfPageRefCoins = useRef(null) - - useEffect(() => { - if (!endOfPageRefCoins.current) { - return - } - - const observer = new IntersectionObserver(entries => { - const endOfPage = entries[0] - if (!endOfPage.isIntersecting) { - return - } - if (isSearching && hasMoreSearchCoins) { - setIsLoading(true) - setTimeout(() => { - fetchMoreSearchCoinBalances() - setIsLoading(false) - }, 500) - } else if (!isSearching && hasMoreCoins) { - setIsLoading(true) - setTimeout(() => { - fetchMoreCoinBalances() - setIsLoading(false) - }, 500) - } - }) - - observer.observe(endOfPageRefCoins.current) - - return () => { - observer.disconnect() - } - }, [fetchMoreCoinBalances, fetchMoreSearchCoinBalances, isSearching]) - - return ( -
-
- {isPending && ( - <> - {Array(8) - .fill(null) - .map((_, i) => ( - - ))} - - )} - {!isPending && displayedCoinBalances.length === 0 && No Coins Found} - {!isPending && - displayedCoinBalances.map((indexItem, index) => { - const balance = coinBalances[indexItem.index] - return - })} - {isLoading && } -
-
-
- ) -} diff --git a/packages/wallet-widget/src/views/Search/components/CollectionsTab.tsx b/packages/wallet-widget/src/views/Search/components/CollectionsTab.tsx deleted file mode 100644 index 62aace2e1..000000000 --- a/packages/wallet-widget/src/views/Search/components/CollectionsTab.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Spinner, Skeleton, Text } from '@0xsequence/design-system' -import { TokenBalance } from '@0xsequence/indexer' -import React, { useEffect, useRef, useState } from 'react' - -import { IndexedData } from '../SearchWalletViewAll' - -import { BalanceItem } from './BalanceItem' - -interface CollectionsTabProps { - displayedCollectionBalances: IndexedData[] - fetchMoreCollectionBalances: () => void - fetchMoreSearchCollectionBalances: () => void - hasMoreCollections: boolean - hasMoreSearchCollections: boolean - isSearching: boolean - isPending: boolean - collectionBalances: TokenBalance[] -} - -export const CollectionsTab: React.FC = ({ - displayedCollectionBalances, - fetchMoreCollectionBalances, - fetchMoreSearchCollectionBalances, - hasMoreCollections, - hasMoreSearchCollections, - isSearching, - isPending, - collectionBalances -}) => { - const [isLoading, setIsLoading] = useState(false) - - const endOfPageRefCollections = useRef(null) - - useEffect(() => { - if (!endOfPageRefCollections.current) { - return - } - - const observer = new IntersectionObserver(entries => { - const endOfPage = entries[0] - if (!endOfPage.isIntersecting) { - return - } - if (isSearching && hasMoreSearchCollections) { - setIsLoading(true) - setTimeout(() => { - fetchMoreSearchCollectionBalances() - setIsLoading(false) - }, 500) - } else if (!isSearching && hasMoreCollections) { - setIsLoading(true) - setTimeout(() => { - fetchMoreCollectionBalances() - setIsLoading(false) - }, 500) - } - }) - observer.observe(endOfPageRefCollections.current) - - return () => { - observer.disconnect() - } - }, [fetchMoreCollectionBalances, fetchMoreSearchCollectionBalances, isSearching]) - - return ( -
-
- {isPending && ( - <> - {Array(8) - .fill(null) - .map((_, i) => ( - - ))} - - )} - {!isPending && displayedCollectionBalances.length === 0 && No Collectibles Found} - {!isPending && - displayedCollectionBalances.map((indexItem, index) => { - const balance = collectionBalances[indexItem.index] - return - })} - {isLoading && } -
-
-
- ) -} diff --git a/packages/wallet-widget/src/views/Search/components/WalletLink.tsx b/packages/wallet-widget/src/views/Search/components/WalletLink.tsx deleted file mode 100644 index 7074b27e2..000000000 --- a/packages/wallet-widget/src/views/Search/components/WalletLink.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Text, ChevronRightIcon } from '@0xsequence/design-system' -import React from 'react' - -import { Navigation } from '../../../contexts' -import { useNavigation } from '../../../hooks' - -interface WalletLinkProps { - toLocation: Navigation - label: string -} - -export const WalletLink = ({ toLocation, label }: WalletLinkProps) => { - const { setNavigation } = useNavigation() - - const onClick = () => { - setNavigation(toLocation) - } - - return ( -
- - {label} - -
- - View all - - -
-
- ) -} diff --git a/packages/wallet-widget/src/views/Search/index.ts b/packages/wallet-widget/src/views/Search/index.ts index 7b706a865..184260b4b 100644 --- a/packages/wallet-widget/src/views/Search/index.ts +++ b/packages/wallet-widget/src/views/Search/index.ts @@ -1,2 +1,2 @@ -export * from './SearchWallet' -export * from './SearchWalletViewAll' +export * from './SearchTokens' +export * from './SearchCollectibles' diff --git a/packages/wallet-widget/src/views/SendCoin.tsx b/packages/wallet-widget/src/views/Send/SendCoin.tsx similarity index 81% rename from packages/wallet-widget/src/views/SendCoin.tsx rename to packages/wallet-widget/src/views/Send/SendCoin.tsx index 9633a78d5..0ccf72d4f 100644 --- a/packages/wallet-widget/src/views/SendCoin.tsx +++ b/packages/wallet-widget/src/views/Send/SendCoin.tsx @@ -7,7 +7,8 @@ import { useCheckWaasFeeOptions, useWaasFeeOptions, waitForTransactionReceipt, - TRANSACTION_CONFIRMATIONS_DEFAULT + TRANSACTION_CONFIRMATIONS_DEFAULT, + useWallets } from '@0xsequence/connect' import { Button, @@ -19,7 +20,8 @@ import { NumericInput, TextInput, Spinner, - Card + Card, + useToast } from '@0xsequence/design-system' import { useClearCachedBalances, @@ -33,12 +35,13 @@ import { useState, ChangeEvent, useRef, useEffect } from 'react' import { encodeFunctionData, formatUnits, parseUnits, toHex, zeroAddress, Hex } from 'viem' import { useAccount, useChainId, useSwitchChain, useConfig, useSendTransaction, usePublicClient } from 'wagmi' -import { SendItemInfo } from '../components/SendItemInfo' -import { TransactionConfirmation } from '../components/TransactionConfirmation' -import { ERC_20_ABI, HEADER_HEIGHT } from '../constants' -import { useNavigationContext } from '../contexts/Navigation' -import { useSettings, useNavigation } from '../hooks' -import { computeBalanceFiat, limitDecimals, isEthAddress } from '../utils' +import { WalletSelect } from '../../components/Select/WalletSelect' +import { SendItemInfo } from '../../components/SendItemInfo' +import { TransactionConfirmation } from '../../components/TransactionConfirmation' +import { ERC_20_ABI, HEADER_HEIGHT_WITH_LABEL } from '../../constants' +import { useNavigationContext } from '../../contexts/Navigation' +import { useSettings, useNavigation } from '../../hooks' +import { computeBalanceFiat, limitDecimals, isEthAddress } from '../../utils' interface SendCoinProps { chainId: number @@ -49,6 +52,8 @@ export const SendCoin = ({ chainId, contractAddress }: SendCoinProps) => { const { clearCachedBalances } = useClearCachedBalances() const publicClient = usePublicClient({ chainId }) const indexerClient = useIndexerClient(chainId) + const toast = useToast() + const { wallets } = useWallets() const { setNavigation } = useNavigation() const { setIsBackButtonEnabled } = useNavigationContext() const { analytics } = useAnalyticsContext() @@ -56,8 +61,8 @@ export const SendCoin = ({ chainId, contractAddress }: SendCoinProps) => { const connectedChainId = useChainId() const { address: accountAddress = '', connector } = useAccount() const isConnectorSequenceBased = !!(connector as ExtendedConnector)?._wallet?.isSequenceBased + const isConnectorWaas = !!(connector as ExtendedConnector)?.type?.includes('waas') const isCorrectChainId = connectedChainId === chainId - const showSwitchNetwork = !isCorrectChainId && !isConnectorSequenceBased const { switchChainAsync } = useSwitchChain() const amountInputRef = useRef(null) const { fiatCurrency } = useSettings() @@ -165,6 +170,15 @@ export const SendCoin = ({ chainId, contractAddress }: SendCoinProps) => { const handleSendClick = async (e: ChangeEvent) => { e.preventDefault() + if (!isCorrectChainId && !isConnectorSequenceBased) { + await switchChainAsync({ chainId }) + } + + if (!isConnectorWaas) { + executeTransaction() + return + } + setIsCheckingFeeOptions(true) const sendAmount = parseUnits(amountToSendFormatted, decimals) @@ -238,6 +252,13 @@ export const SendCoin = ({ chainId, contractAddress }: SendCoinProps) => { clearCachedBalances() } } + setIsSendTxnPending(false) + + toast({ + title: 'Transaction sent', + description: `Successfully sent ${amountToSendFormatted} ${symbol} to ${toAddress}`, + variant: 'success' + }) } } @@ -264,9 +285,9 @@ export const SendCoin = ({ chainId, contractAddress }: SendCoinProps) => { return (
@@ -298,17 +319,9 @@ export const SendCoin = ({ chainId, contractAddress }: SendCoinProps) => { {`~${fiatCurrency.sign}${amountToSendFiat}`}
@@ -328,59 +341,40 @@ export const SendCoin = ({ chainId, contractAddress }: SendCoinProps) => { ) : ( - setToAddress(ev.target.value)} - placeholder={`${nativeTokenInfo.name} Address (0x...)`} - name="to-address" - data-1p-ignore - controls={ -
- {showSwitchNetwork && ( -
- - The wallet is connected to the wrong network. Please switch network before proceeding - -
- )} - -
+
{isCheckingFeeOptions ? ( ) : (
- {showSwitchNetwork && ( -
- - The wallet is connected to the wrong network. Please switch network before proceeding - -
- )} - -
+
{isCheckingFeeOptions ? ( ) : (
+
+
+ + + + Manage Wallets + + + + + + Manage Networks + + + + + + Manage Currency + + + {/* {isEmbedded && ( + + + Manage Profiles + + + )} */} + + + Preferences + +
) diff --git a/packages/wallet-widget/src/views/Settings/Network.tsx b/packages/wallet-widget/src/views/Settings/Network.tsx deleted file mode 100644 index 26792b31b..000000000 --- a/packages/wallet-widget/src/views/Settings/Network.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useWalletSettings } from '@0xsequence/connect' -import { Text, TokenImage } from '@0xsequence/design-system' -import { ChainId } from '@0xsequence/network' -import { useConfig } from 'wagmi' - -import { SelectButton } from '../../components/SelectButton' -import { HEADER_HEIGHT } from '../../constants' -import { useSettings } from '../../hooks' - -export const SettingsNetwork = () => { - const { readOnlyNetworks, displayedAssets } = useWalletSettings() - const { selectedNetworks, setSelectedNetworks } = useSettings() - const { chains } = useConfig() - - const allChains = [ - ...new Set([...chains.map(chain => chain.id), ...(readOnlyNetworks || []), ...displayedAssets.map(asset => asset.chainId)]) - ] - - const onClickNetwork = (chainId: number) => { - if (selectedNetworks.includes(chainId)) { - if (selectedNetworks.length === 1) { - return - } - setSelectedNetworks(selectedNetworks.filter(id => id !== chainId)) - } else { - setSelectedNetworks([...selectedNetworks, chainId]) - } - } - - return ( -
-
- - Networks - -
- {allChains.map(chain => { - return ( - onClickNetwork(chain)} - value={chain} - squareIndicator - > -
- - - {ChainId[chain]} - -
-
- ) - })} -
-
-
- ) -} diff --git a/packages/wallet-widget/src/views/Settings/Networks.tsx b/packages/wallet-widget/src/views/Settings/Networks.tsx new file mode 100644 index 000000000..d2e90ba7a --- /dev/null +++ b/packages/wallet-widget/src/views/Settings/Networks.tsx @@ -0,0 +1,9 @@ +import { NetworksFilter } from '../../components/Filter/NetworksFilter' + +export const SettingsNetworks = () => { + return ( +
+ +
+ ) +} diff --git a/packages/wallet-widget/src/views/Settings/Preferences.tsx b/packages/wallet-widget/src/views/Settings/Preferences.tsx new file mode 100644 index 000000000..c7e7c4584 --- /dev/null +++ b/packages/wallet-widget/src/views/Settings/Preferences.tsx @@ -0,0 +1,26 @@ +import { Switch, Text } from '@0xsequence/design-system' +import { useObservable } from 'micro-observables' + +import { ListCardSelect } from '../../components/ListCard/ListCardSelect' +import { HEADER_HEIGHT } from '../../constants' +import { useSettings } from '../../hooks' + +export const SettingsPreferences = () => { + const { hideUnlistedTokensObservable, setHideUnlistedTokens } = useSettings() + const hideUnlistedTokens = useObservable(hideUnlistedTokensObservable) + + return ( +
+ } + type="custom" + onClick={() => setHideUnlistedTokens(!hideUnlistedTokens)} + > + + Hide Unlisted Tokens + + +
+ ) +} diff --git a/packages/wallet-widget/src/views/Settings/Profiles.tsx b/packages/wallet-widget/src/views/Settings/Profiles.tsx new file mode 100644 index 000000000..d89664dd6 --- /dev/null +++ b/packages/wallet-widget/src/views/Settings/Profiles.tsx @@ -0,0 +1,9 @@ +import { Text } from '@0xsequence/design-system' + +export const SettingsProfiles = () => { + return ( +
+ Profiles +
+ ) +} diff --git a/packages/wallet-widget/src/views/Settings/QrScan.tsx b/packages/wallet-widget/src/views/Settings/QrScan.tsx new file mode 100644 index 000000000..b01af201d --- /dev/null +++ b/packages/wallet-widget/src/views/Settings/QrScan.tsx @@ -0,0 +1,94 @@ +import { Button, Card, TabsContent, TabsHeader, TabsRoot, Text, TextInput, useMediaQuery } from '@0xsequence/design-system' +import { Scanner } from '@yudiel/react-qr-scanner' +import { ChangeEvent, useState } from 'react' + +import { HEADER_HEIGHT } from '../../constants' +// import { useWalletConnectContext } from '../../contexts/WalletConnect' +import { useNavigation } from '../../hooks' + +export function QrScan() { + const isMobile = useMediaQuery('isMobile') + const { goBack } = useNavigation() + const [signClientUri, setSignClientUri] = useState('') + const [selectedTab, setSelectedTab] = useState<'scan' | 'paste'>('scan') + // const { pair } = useWalletConnectContext() + + const handleSignClientUri = async () => { + if (signClientUri) { + console.log(signClientUri) + try { + // await pair(signClientUri) + } catch (error) { + console.error('Error pairing with dapp', error) + return + } + } + } + + return ( +
+ + Connect to a DApp using WalletConnect + + setSelectedTab(value as 'scan' | 'paste')} + > + + + + { + if (result[0].rawValue) { + setSignClientUri(result[0].rawValue) + } + }} + styles={{ + video: { + transform: isMobile ? 'scaleX(-1)' : 'scaleX(1)', + borderRadius: '10px' + } + }} + /> + + + + ) => setSignClientUri(ev.target.value)} + /> +
+
+
+
+
+ ) +} diff --git a/packages/wallet-widget/src/views/Settings/Wallets.tsx b/packages/wallet-widget/src/views/Settings/Wallets.tsx new file mode 100644 index 000000000..20d472ca5 --- /dev/null +++ b/packages/wallet-widget/src/views/Settings/Wallets.tsx @@ -0,0 +1,125 @@ +import { formatAddress, useOpenConnectModal, useWallets } from '@0xsequence/connect' +import { cardVariants, cn, Text, Divider, IconButton, CheckmarkIcon, CloseIcon, Spinner } from '@0xsequence/design-system' +import { useState } from 'react' + +import { CopyButton } from '../../components/CopyButton' +import { MediaIconWrapper } from '../../components/IconWrappers' +import { ListCardSelect } from '../../components/ListCard/ListCardSelect' +import { WalletAccountGradient } from '../../components/WalletAccountGradient' + +export const SettingsWallets = () => { + const { wallets, disconnectWallet } = useWallets() + const { setOpenConnectModal } = useOpenConnectModal() + + const [disconnectConfirm, setDisconnectConfirm] = useState(null) + const [isUnlinking, setIsUnlinking] = useState(false) + + const onClickAddWallet = () => { + setOpenConnectModal(true) + } + + const DisconnectButton = ({ label, onClick }: { label: string; onClick: () => void }) => { + return ( +
+ + {label} + +
+ ) + } + + const confrimDisconnectAll = () => { + setDisconnectConfirm('All') + } + + const confirmDisconnect = (address: string) => { + setDisconnectConfirm(address) + } + + const handleDisconnect = async () => { + setIsUnlinking(true) + if (disconnectConfirm === 'All') { + wallets.forEach(async wallet => await disconnectWallet(wallet.address)) + } else { + await disconnectWallet(disconnectConfirm || '') + } + setDisconnectConfirm(null) + setIsUnlinking(false) + } + + return ( +
+
+ {wallets.length > 1 && ( + + ) : disconnectConfirm === 'All' ? ( +
+ handleDisconnect()} /> + setDisconnectConfirm(null)} /> +
+ ) : ( + confrimDisconnectAll()} /> + ) + } + > + wallet.address)} size="sm" isAccount /> + + All + +
+ )} + {wallets.map(wallet => ( + + ) : disconnectConfirm === wallet.address ? ( +
+ handleDisconnect()} /> + setDisconnectConfirm(null)} /> +
+ ) : ( + confirmDisconnect(wallet.address)} /> + ) + } + > + + + {formatAddress(wallet.address)} + + +
+ ))} +
+ +
+ +
+
onClickAddWallet()} + > + + Add Wallet + +
+
+
+
+ ) +} diff --git a/packages/wallet-widget/src/views/Settings/index.ts b/packages/wallet-widget/src/views/Settings/index.ts index f4da7c858..ef634c3e1 100644 --- a/packages/wallet-widget/src/views/Settings/index.ts +++ b/packages/wallet-widget/src/views/Settings/index.ts @@ -1,4 +1,8 @@ export * from './Menu' -export * from './General' +export * from './Wallets' +export * from './Profiles' +export * from './Apps' export * from './Currency' -export * from './Network' +export * from './Networks' +export * from './Preferences' +export * from './QrScan' diff --git a/packages/wallet-widget/src/views/Swap/CoinInput.tsx b/packages/wallet-widget/src/views/Swap/CoinInput.tsx new file mode 100644 index 000000000..0156940ab --- /dev/null +++ b/packages/wallet-widget/src/views/Swap/CoinInput.tsx @@ -0,0 +1,81 @@ +import { Button, NumericInput, Text } from '@0xsequence/design-system' +import { ChangeEvent, useEffect, useState } from 'react' +import { formatUnits } from 'viem' + +import { useSettings } from '../../hooks' +import { useSwap } from '../../hooks/useSwap' +import { formatFiatBalance, decimalsToWei } from '../../utils/formatBalance' + +export const CoinInput = ({ type, disabled }: { type: 'from' | 'to'; disabled?: boolean }) => { + const { toCoin, fromCoin, amount, nonRecentAmount, recentInput, setAmount } = useSwap() + const coin = type === 'from' ? fromCoin : toCoin + + const { fiatCurrency } = useSettings() + + const [inputValue, setInputValue] = useState('') + + const fiatBalance = formatFiatBalance( + type === recentInput ? amount : nonRecentAmount, + coin?.price.value || 0, + coin?.contractInfo?.decimals || 18, + fiatCurrency.sign + ) + + useEffect(() => { + if (type === recentInput) { + if (amount > 0) { + setInputValue(formatUnits(BigInt(amount), coin?.contractInfo?.decimals || 18)) + } else if (Number(inputValue)) { + setInputValue('') + } + } else if (type !== recentInput) { + if (nonRecentAmount > 0) { + setInputValue(formatUnits(BigInt(nonRecentAmount), coin?.contractInfo?.decimals || 18)) + } else if (Number(inputValue)) { + setInputValue('') + } + } + }, [recentInput, amount, nonRecentAmount]) + + const handleChange = (ev: ChangeEvent) => { + const { value } = ev.target + const changedValue = Number(value) + setInputValue(value) + setAmount(decimalsToWei(changedValue, coin?.contractInfo?.decimals || 18), type) + } + + const handleMax = () => { + setAmount(Number(formatUnits(BigInt(coin?.balance || 0), coin?.contractInfo?.decimals || 18)), type) + } + + return ( +
+ + {fiatBalance && ( + + ~{fiatBalance} + + )} + {type === 'from' && ( +
+ ) +} diff --git a/packages/wallet-widget/src/views/Swap/CoinSelect.tsx b/packages/wallet-widget/src/views/Swap/CoinSelect.tsx new file mode 100644 index 000000000..6f41a9e24 --- /dev/null +++ b/packages/wallet-widget/src/views/Swap/CoinSelect.tsx @@ -0,0 +1,82 @@ +import { cn, cardVariants, Text, ChevronDownIcon, TokenImage } from '@0xsequence/design-system' +import { AnimatePresence } from 'motion/react' +import { useState } from 'react' +import { useChains } from 'wagmi' + +import { CoinRow } from '../../components/SearchLists/TokenList/CoinRow' +import { SlideupDrawer } from '../../components/Select/SlideupDrawer' +import { useSettings } from '../../hooks' +import { useSwap } from '../../hooks/useSwap' +import { TokenBalanceWithPrice } from '../../utils' +import { formatTokenInfo } from '../../utils/formatBalance' + +export const CoinSelect = ({ + selectType, + coinList, + disabled +}: { + selectType: 'from' | 'to' + coinList: TokenBalanceWithPrice[] + disabled?: boolean +}) => { + const { fromCoin, toCoin, setFromCoin, setToCoin } = useSwap() + const { fiatCurrency } = useSettings() + const chains = useChains() + + const selectedCoin = selectType === 'from' ? fromCoin : toCoin + const setSelectedCoin = selectType === 'from' ? setFromCoin : setToCoin + + const [isSelectorOpen, setIsSelectorOpen] = useState(false) + + const { logo, name, symbol, displayBalance } = formatTokenInfo(selectedCoin, fiatCurrency.sign, chains) + + const handleSelect = (coin: TokenBalanceWithPrice) => { + setSelectedCoin(coin) + setIsSelectorOpen(false) + } + + return ( +
+
setIsSelectorOpen(true)} + > +
+ + {selectType === 'from' ? 'From' : 'To'} + + {selectedCoin ? ( +
+ +
+ + {name} + + + {displayBalance} + +
+
+ ) : ( + + Select Coin + + )} +
+ +
+ + + {isSelectorOpen && ( + setIsSelectorOpen(false)}> +
+ {coinList.map((coin, index) => ( + + ))} +
+
+ )} +
+
+ ) +} diff --git a/packages/wallet-widget/src/views/Swap/Swap.tsx b/packages/wallet-widget/src/views/Swap/Swap.tsx new file mode 100644 index 000000000..b8c1c99a5 --- /dev/null +++ b/packages/wallet-widget/src/views/Swap/Swap.tsx @@ -0,0 +1,136 @@ +import { ArrowRightIcon, Card, cardVariants, cn, IconButton, Spinner, Text } from '@0xsequence/design-system' +import { useGetCoinPrices, useGetExchangeRate, useGetTokenBalancesSummary } from '@0xsequence/hooks' +import { useEffect } from 'react' +import { useAccount, useChainId } from 'wagmi' + +import { NetworkSelect } from '../../components/Select/NetworkSelect' +import { HEADER_HEIGHT_WITH_LABEL } from '../../constants' +import { useSettings } from '../../hooks' +import { useSwap } from '../../hooks/useSwap' + +import { CoinInput } from './CoinInput' +import { CoinSelect } from './CoinSelect' + +export const Swap = () => { + const { + fromCoin, + toCoin, + isSwapReady, + isSwapQuotePending, + hasInsufficientFunds, + isErrorSwapQuote, + isTxnPending, + switchCoinOrder, + onSubmitSwap, + resetSwapStates + } = useSwap() + const { fiatCurrency } = useSettings() + const { address: userAddress } = useAccount() + const connectedChainId = useChainId() + + useEffect(() => { + return resetSwapStates() + }, []) + + const { data: tokenBalances } = useGetTokenBalancesSummary({ + chainIds: [connectedChainId], + filter: { + accountAddresses: [String(userAddress)], + omitNativeBalances: false + } + }) + + const coinBalances = tokenBalances?.filter(c => c.contractType !== 'ERC1155' && c.contractType !== 'ERC721') || [] + + const { data: coinPrices = [] } = useGetCoinPrices( + coinBalances.map(token => ({ + chainId: token.chainId, + contractAddress: token.contractAddress + })) + ) + + const { data: conversionRate = 1 } = useGetExchangeRate(fiatCurrency.symbol) + + const coinBalancesWithPrices = coinBalances.map(balance => { + const matchingPrice = coinPrices.find(price => { + const isSameChainAndAddress = + price.token.chainId === balance.chainId && price.token.contractAddress === balance.contractAddress + + const isTokenIdMatch = + price.token.tokenId === balance.tokenID || !(balance.contractType === 'ERC721' || balance.contractType === 'ERC1155') + + return isSameChainAndAddress && isTokenIdMatch + }) + + const priceValue = (matchingPrice?.price?.value || 0) * conversionRate + const priceCurrency = fiatCurrency.symbol + + return { + ...balance, + price: { value: priceValue, currency: priceCurrency } + } + }) + + return ( +
+ +
+ + + {fromCoin && } + {/* TODO: change out disabled to isTxnPending after new swap api is implemented */} + {isErrorSwapQuote && + (hasInsufficientFunds ? ( + + Insufficient Funds + + ) : ( + + Something went wrong. Try again later or with a different combination of coins. + + ))} + +
+
+ +
+
+
+ + + {toCoin && } + {isSwapQuotePending && ( +
+ + + Fetching best price quote… + +
+ )} + {isTxnPending && ( +
+ + + Sending swap transaction… + +
+ )} +
+
+
+ + Swap + +
+
+ ) +} diff --git a/packages/wallet-widget/src/views/Swap/index.ts b/packages/wallet-widget/src/views/Swap/index.ts new file mode 100644 index 000000000..0fba60808 --- /dev/null +++ b/packages/wallet-widget/src/views/Swap/index.ts @@ -0,0 +1 @@ +export * from './Swap' diff --git a/packages/wallet-widget/src/views/SwapCoin/SwapList.tsx b/packages/wallet-widget/src/views/SwapCoin/SwapList.tsx index 0b4d232fc..235e467f0 100644 --- a/packages/wallet-widget/src/views/SwapCoin/SwapList.tsx +++ b/packages/wallet-widget/src/views/SwapCoin/SwapList.tsx @@ -172,7 +172,8 @@ export const SwapList = ({ chainId, contractAddress, amount }: SwapListProps) => location: 'coin-details', params: { chainId, - contractAddress + contractAddress, + accountAddress: userAddress } }) } catch (e) { diff --git a/packages/wallet-widget/src/views/TransactionDetails/index.tsx b/packages/wallet-widget/src/views/TransactionDetails/index.tsx index e8cdf4e74..d36c0eafa 100644 --- a/packages/wallet-widget/src/views/TransactionDetails/index.tsx +++ b/packages/wallet-widget/src/views/TransactionDetails/index.tsx @@ -12,7 +12,7 @@ import { TokenImage } from '@0xsequence/design-system' import { useGetCoinPrices, useGetCollectiblePrices, useGetExchangeRate } from '@0xsequence/hooks' -import { Transaction, TxnTransfer } from '@0xsequence/indexer' +import { Transaction, TxnTransfer, TxnTransferType } from '@0xsequence/indexer' import dayjs from 'dayjs' import { formatUnits, zeroAddress } from 'viem' import { useConfig } from 'wagmi' @@ -91,8 +91,91 @@ export const TransactionDetails = ({ transaction }: TransactionDetailProps) => { const recipientAddressFormatted = recipientAddress.substring(0, 10) + '...' + recipientAddress.substring(transfer.to.length - 4, transfer.to.length) const isNativeToken = compareAddress(transfer?.contractInfo?.address || '', zeroAddress) - const logoURI = isNativeToken ? nativeTokenInfo.logoURI : transfer?.contractInfo?.logoURI - const symbol = isNativeToken ? nativeTokenInfo.symbol : transfer?.contractInfo?.symbol || '' + const isCollectible = transfer.contractType === 'ERC721' || transfer.contractType === 'ERC1155' + const tokenId = transfer.tokenIds?.[0] + const tokenLogoURI = isNativeToken + ? nativeTokenInfo.logoURI + : isCollectible + ? transfer?.tokenMetadata?.[String(tokenId)]?.image + : transfer?.contractInfo?.logoURI + const contractLogoURI = transfer?.contractInfo?.logoURI + const tokenSymbol = isNativeToken + ? nativeTokenInfo.symbol + : isCollectible + ? transfer?.tokenMetadata?.[String(tokenId)]?.name || '' + : transfer?.contractInfo?.symbol || '' + const contractSymbol = transfer?.contractInfo?.name || '' + + const WalletContent = () => ( +
+ + + {recipientAddressFormatted} + +
+ ) + + const TokenContent = ({ + balanceDisplayed, + fiatValue, + fiatPrice + }: { + balanceDisplayed: string + fiatValue: string + fiatPrice: number + }) => ( +
+ {isCollectible && ( +
+ + + {contractSymbol} + +
+ )} +
+ +
+ + {`${balanceDisplayed} ${tokenSymbol}`} + + {arePricesLoading ? ( + + ) : ( + + {fiatPrice ? `${fiatCurrency.sign}${fiatValue}` : ''} + + )} +
+
+
+ ) return ( <> @@ -119,36 +202,23 @@ export const TransactionDetails = ({ transaction }: TransactionDetailProps) => { const fiatValue = (parseFloat(formattedBalance) * (conversionRate * (fiatPrice || 0))).toFixed(2) + const isReceiveTransfer = transfer.transferType === TxnTransferType.RECEIVE + return (
-
- -
- - {`${balanceDisplayed} ${symbol}`} - - {arePricesLoading ? ( - - ) : ( - - {fiatPrice ? `${fiatCurrency.sign}${fiatValue}` : ''} - - )} -
-
- -
- - - {recipientAddressFormatted} - -
+ {isReceiveTransfer ? ( + <> + + + + + ) : ( + <> + + + + + )}
) })} diff --git a/packages/wallet-widget/src/views/index.ts b/packages/wallet-widget/src/views/index.ts index c300c9c9b..afb06a806 100644 --- a/packages/wallet-widget/src/views/index.ts +++ b/packages/wallet-widget/src/views/index.ts @@ -1,13 +1,12 @@ export * from './Home' export * from './Receive' -export * from './SendCoin' -export * from './SendCollectible' +export * from './Send' export * from './History' export * from './Search' export * from './Settings' export * from './CoinDetails' -export * from './CollectionDetails' export * from './CollectibleDetails' export * from './TransactionDetails' export * from './SwapCoin' export * from './SwapCoin/SwapList' +export * from './Swap/Swap' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 582778d78..6856c614a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,7 +82,7 @@ importers: version: 2.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(motion@12.4.10(@emotion/is-prop-valid@0.8.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@0xsequence/network': specifier: '*' - version: 2.2.13(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 2.3.9(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@radix-ui/react-popover': specifier: ^1.0.7 version: 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -546,6 +546,9 @@ importers: fuse.js: specifier: ^6.6.2 version: 6.6.2 + micro-observables: + specifier: 1.7.2 + version: 1.7.2(react@19.0.0) motion: specifier: ^12.3.1 version: 12.4.10(@emotion/is-prop-valid@0.8.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -556,6 +559,9 @@ importers: specifier: ^5.1.0 version: 5.1.0(react@19.0.0) devDependencies: + '@0xsequence/checkout': + specifier: workspace:* + version: link:../checkout '@0xsequence/connect': specifier: workspace:* version: link:../connect @@ -565,6 +571,9 @@ importers: '@types/react-copy-to-clipboard': specifier: ^5.0.7 version: 5.0.7 + '@yudiel/react-qr-scanner': + specifier: 2.2.1 + version: 2.2.1(@types/emscripten@1.40.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ethers: specifier: ^6.13.0 version: 6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -588,9 +597,6 @@ packages: peerDependencies: ethers: '>=6' - '@0xsequence/abi@2.2.13': - resolution: {integrity: sha512-cZDR83SNxpXTD9vZoJAtmn6/YgvttN0gmeB8feQ5cpo8yd+AeJvQpUGMPR6Ud1MQGsvcPTLnTwKkL432Pc6UGQ==} - '@0xsequence/abi@2.3.9': resolution: {integrity: sha512-5bFZIerqx4VMxny5PdFAGH4uyZhzvsH3Ri1/U0Tr2a05t5jSSXM5YVCf8QzWpSS51lpPRknMrvuuBUfEcV2dgw==} @@ -607,11 +613,6 @@ packages: peerDependencies: ethers: '>=6' - '@0xsequence/core@2.2.13': - resolution: {integrity: sha512-tu6OEGbDLg1KfMb+YOn0Z5BsYbK3UFBjuPVHRx7SpmCsZPEApJqqRKHpTYah2T3bmYIJy9tRBtqstfTsniSgng==} - peerDependencies: - ethers: '>=6' - '@0xsequence/core@2.3.9': resolution: {integrity: sha512-N97jPBe//72upmkelaypiSRLhGzVIHttOxRGa2w8b3lH6lNJ6tVHYkcrCU3I8GCnnURyINbVYR+r2C2s0+2sYQ==} peerDependencies: @@ -634,9 +635,6 @@ packages: peerDependencies: ethers: '>=6' - '@0xsequence/indexer@2.2.13': - resolution: {integrity: sha512-RFjjjckuAhM0vgYFO1RgYyBqsUrU54l2N/Isr5DPSLCCczp+qcopNuFMB0e0FuvzUP1buunxTjHRVjFH75kQEA==} - '@0xsequence/indexer@2.3.9': resolution: {integrity: sha512-ZtiA7827BTjujOupom5dGJd4oMvfQA2HQ9orjrreZusQPmfadie9ldVrnzX19YS+AZDeCOix9qDG+cRSpYfHwQ==} @@ -651,11 +649,6 @@ packages: peerDependencies: ethers: '>=6' - '@0xsequence/network@2.2.13': - resolution: {integrity: sha512-U8sVC2nWokPtQzIXwNKOP/mgkkuvjxYmQrgITkc8YDTAQOYPu6n/4lIqQ2//jjSfloniMK00tDpfUd5fCFbsUA==} - peerDependencies: - ethers: '>=6' - '@0xsequence/network@2.3.9': resolution: {integrity: sha512-QFvGsfkjkuENI3yrBAil1Axm0iBK/BMO8nqEo5MZ8zxYPkOG3I4Lyjr28s4Ny2aOUzaV8I3R4udTOM+S6b5vXw==} peerDependencies: @@ -666,11 +659,6 @@ packages: peerDependencies: ethers: '>=6' - '@0xsequence/relayer@2.2.13': - resolution: {integrity: sha512-fnt4AJ1u9CPwx7JJwpZ7UbJ3eN2LOG0Wuwe4FS9OSfxCOhbokELRSWegToEyH6ew6FRPvsuRMN/im3Bu3Og0QQ==} - peerDependencies: - ethers: '>=6' - '@0xsequence/relayer@2.3.9': resolution: {integrity: sha512-Ta9vYWfl38nNj1HVgf8igjH41oj5aY+ZK923Rio1ms5D4WWR6oSoHBBFRmbU5qT8HzTa8GWoYrxhohHcPV5S6g==} peerDependencies: @@ -691,11 +679,6 @@ packages: peerDependencies: ethers: '>=6' - '@0xsequence/utils@2.2.13': - resolution: {integrity: sha512-V4uip1fCZAzp5O2S+nkKnwrqmzzC7em1Mc4HJvUX+fqT0jzw20BZt0CNlX34DgW6E6MzBvWnrX+DTfz/+alBWQ==} - peerDependencies: - ethers: '>=6' - '@0xsequence/utils@2.3.9': resolution: {integrity: sha512-dZI3dWkHABkRj39fp+GdieadQiXzntosrClwYKf7wT780/e6DlBMZV2tb/iI+j73Qq64+MfSheimEw7Tjk+kww==} peerDependencies: @@ -3431,6 +3414,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/emscripten@1.40.0': + resolution: {integrity: sha512-MD2JJ25S4tnjnhjWyalMS6K6p0h+zQV6+Ylm+aGbiS8tSn/aHLSGNzBgduj6FB4zH0ax2GRMGYi/8G1uOxhXWA==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -3771,6 +3757,12 @@ packages: '@walletconnect/window-metadata@1.0.1': resolution: {integrity: sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==} + '@yudiel/react-qr-scanner@2.2.1': + resolution: {integrity: sha512-5tmzoi9d8mqqaxKTxfI9kulS3N3Kph75AOqwU2lnl4IMwjmLcTXS8IyecDUvzFNuoMNhCpofrqqGueXm8IXkjA==} + peerDependencies: + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + abitype@1.0.8: resolution: {integrity: sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==} peerDependencies: @@ -3959,6 +3951,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + barcode-detector@3.0.1: + resolution: {integrity: sha512-3fCzG/Py4SVgZJhubD1mt7rVprtHEVWrxQN4FUOG0oulPE4193evbgyptxcOYsfTNEtMlWc+Ec9tdxhjlR4/Ww==} + base-x@3.0.11: resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==} @@ -5232,6 +5227,9 @@ packages: hmac-drbg@1.0.1: resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -5846,6 +5844,12 @@ packages: micro-ftch@0.3.1: resolution: {integrity: sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==} + micro-observables@1.7.2: + resolution: {integrity: sha512-NLXMvBpeXO/QAJ9r2SoxAE9qJXvECMRnKXkQrjZ5TNy0bUKawrgyUafHbJpFnP3j+DTehjPxJU6VXnNMjvicNA==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16.8.0' + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -6745,6 +6749,9 @@ packages: scrypt-js@3.0.1: resolution: {integrity: sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==} + sdp@3.2.0: + resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==} + secp256k1@4.0.4: resolution: {integrity: sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==} engines: {node: '>=18.0.0'} @@ -7724,6 +7731,10 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webrtc-adapter@9.0.1: + resolution: {integrity: sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==} + engines: {node: '>=6.0.0', npm: '>=3.10.0'} + websocket@1.0.35: resolution: {integrity: sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==} engines: {node: '>=4.0.0'} @@ -7958,6 +7969,11 @@ packages: use-sync-external-store: optional: true + zxing-wasm@2.1.0: + resolution: {integrity: sha512-CvuwDZHRHwg6PeCARaCDIp3dauD4cin0mbHrQQZtMDrr5mblPzCimAjdw/XMhD/Au10q/f5+SAupvYqYvUOg1Q==} + peerDependencies: + '@types/emscripten': '>=1.39.6' + snapshots: 0xsequence@2.3.9(ethers@6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): @@ -7980,8 +7996,6 @@ snapshots: '@0xsequence/wallet': 2.3.9(ethers@6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) ethers: 6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@0xsequence/abi@2.2.13': {} - '@0xsequence/abi@2.3.9': {} '@0xsequence/account@2.3.9(ethers@6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': @@ -8015,12 +8029,6 @@ snapshots: '@0xsequence/wallet': 2.3.9(ethers@6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) ethers: 6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@0xsequence/core@2.2.13(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@0xsequence/abi': 2.2.13 - '@0xsequence/utils': 2.2.13(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - ethers: 6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@0xsequence/core@2.3.9(ethers@6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@0xsequence/abi': 2.3.9 @@ -8073,8 +8081,6 @@ snapshots: '@0xsequence/utils': 2.3.9(ethers@6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) ethers: 6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@0xsequence/indexer@2.2.13': {} - '@0xsequence/indexer@2.3.9': {} '@0xsequence/marketplace@2.2.7': {} @@ -8088,14 +8094,6 @@ snapshots: '@0xsequence/wallet': 2.3.9(ethers@6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) ethers: 6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@0xsequence/network@2.2.13(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@0xsequence/core': 2.2.13(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@0xsequence/indexer': 2.2.13 - '@0xsequence/relayer': 2.2.13(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@0xsequence/utils': 2.2.13(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - ethers: 6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@0xsequence/network@2.3.9(ethers@6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@0xsequence/core': 2.3.9(ethers@6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -8128,13 +8126,6 @@ snapshots: eventemitter2: 6.4.9 webextension-polyfill: 0.10.0 - '@0xsequence/relayer@2.2.13(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@0xsequence/abi': 2.2.13 - '@0xsequence/core': 2.2.13(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@0xsequence/utils': 2.2.13(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - ethers: 6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@0xsequence/relayer@2.3.9(ethers@6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@0xsequence/abi': 2.3.9 @@ -8169,11 +8160,6 @@ snapshots: '@0xsequence/core': 2.3.9(ethers@6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) ethers: 6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@0xsequence/utils@2.2.13(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - ethers: 6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) - js-base64: 3.7.7 - '@0xsequence/utils@2.3.9(ethers@6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: ethers: 6.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -9581,7 +9567,7 @@ snapshots: '@imtbl/passport@2.1.15(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: - '@0xsequence/abi': 2.2.13 + '@0xsequence/abi': 2.3.9 '@0xsequence/core': 2.3.9(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@imtbl/config': 2.1.15 '@imtbl/generated-clients': 2.1.15 @@ -11642,6 +11628,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/emscripten@1.40.0': {} + '@types/estree@1.0.6': {} '@types/graceful-fs@4.1.9': @@ -12500,6 +12488,15 @@ snapshots: '@walletconnect/window-getters': 1.0.1 tslib: 1.14.1 + '@yudiel/react-qr-scanner@2.2.1(@types/emscripten@1.40.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + barcode-detector: 3.0.1(@types/emscripten@1.40.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + webrtc-adapter: 9.0.1 + transitivePeerDependencies: + - '@types/emscripten' + abitype@1.0.8(typescript@5.8.2): optionalDependencies: typescript: 5.8.2 @@ -12722,6 +12719,12 @@ snapshots: balanced-match@1.0.2: {} + barcode-detector@3.0.1(@types/emscripten@1.40.0): + dependencies: + zxing-wasm: 2.1.0(@types/emscripten@1.40.0) + transitivePeerDependencies: + - '@types/emscripten' + base-x@3.0.11: dependencies: safe-buffer: 5.2.1 @@ -14414,6 +14417,10 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -15079,6 +15086,11 @@ snapshots: micro-ftch@0.3.1: {} + micro-observables@1.7.2(react@19.0.0): + dependencies: + hoist-non-react-statics: 3.3.2 + react: 19.0.0 + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -16067,6 +16079,8 @@ snapshots: scrypt-js@3.0.1: {} + sdp@3.2.0: {} + secp256k1@4.0.4: dependencies: elliptic: 6.6.1 @@ -17102,6 +17116,10 @@ snapshots: webidl-conversions@7.0.0: {} + webrtc-adapter@9.0.1: + dependencies: + sdp: 3.2.0 + websocket@1.0.35: dependencies: bufferutil: 4.0.9 @@ -17344,3 +17362,8 @@ snapshots: '@types/react': 19.0.10 react: 19.0.0 use-sync-external-store: 1.4.0(react@19.0.0) + + zxing-wasm@2.1.0(@types/emscripten@1.40.0): + dependencies: + '@types/emscripten': 1.40.0 + type-fest: 4.37.0