diff --git a/README.md b/README.md index 70a8d52..9b26132 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,13 @@ Rivet is still the best dev tool that works well in the development flow of use ## Download -- **Chromium (Chrome, Brave, Arc, etc)** [TBD] +- **Chromium (Chrome, Brave, Arc, etc)** [[Download]](https://chromewebstore.google.com/detail/emajibkjkkilgdahffjhapcljjhhdpeb?utm_source=item-share-cb) - **Firefox**: coming soon - **Safari**: coming soon ### Nightly Release -DW: DevWallet is currently in active development. If you would like to try out the latest features, you can download the latest nightly build below: +DevWallet is currently in active development. If you would like to try out the latest features, you can download the latest nightly build below: - **Chromium (Chrome, Brave, Arc, etc)**: [Download](https://github.com/D01-DayOne/dev-wallet/releases/latest) @@ -147,11 +147,19 @@ bun run dev This will run a script that will build the Web Extension, start a dev server for the Test Dapp, and automatically open Chrome with a fresh profile and the extension installed. -## Known Issues +## Multi-Wallet Support -DW: DevWallet uses the `window.ethereum` interface, which means it has some known conflicts with other wallets which also rely on `window.ethereum`. Once Dapps start to integrate [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963) to handle multiple injected wallets, this should not be a problem anymore. +DW: DevWallet supports both legacy `window.ethereum` injection and modern [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963) multi-wallet discovery. -For best results it is recommended to run DW: DevWallet in its own Chrome profile, without any other conflicting browser wallets installed. +**EIP-6963 Support:** +- DevWallet announces itself via EIP-6963, allowing dapps to discover it alongside other wallets +- If another wallet (MetaMask, Coinbase, etc.) is already installed, DevWallet will NOT overwrite `window.ethereum` to avoid conflicts +- Dapps that support EIP-6963 can present users with a wallet selector to choose between DevWallet and other installed wallets +- The test dapp (`bun run dapp`) demonstrates EIP-6963 wallet discovery + +**Legacy Dapp Compatibility:** +- If no other wallet is present, DevWallet will inject itself as `window.ethereum` for compatibility with older dapps +- For testing legacy dapps, run DevWallet in its own Chrome profile without other wallets installed Helpful note: A fresh Chrome profile gets instantiated when running the dev script: `bun run dev`. diff --git a/src/components/Container.tsx b/src/components/Container.tsx index 2c19d1a..bf520b1 100644 --- a/src/components/Container.tsx +++ b/src/components/Container.tsx @@ -1,15 +1,6 @@ import type { ReactNode } from 'react' import { useNavigate } from 'react-router' -import { - Box, - Button, - Inline, - Inset, - Row, - Rows, - Separator, - Text, -} from '~/design-system' +import { Box, Button, Inset, Row, Rows, Separator, Text } from '~/design-system' import type { RowProps } from '~/design-system/components/Rows' export function Container({ @@ -48,19 +39,17 @@ export function Container({ display="flex" paddingHorizontal="8px" width="full" - style={{ minHeight: '44px' }} + style={{ minHeight: '44px', gap: '12px' }} > - + {typeof header === 'string' ? ( {header} ) : ( header )} - {dismissable && ( + + {dismissable && ( + - )} - + + )} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 1910ece..ef6afd4 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,4 +1,11 @@ -import { type ReactNode, useLayoutEffect, useMemo, useState } from 'react' +import { + type ReactNode, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react' import { Link } from 'react-router-dom' import { type Hex, formatGwei } from 'viem' @@ -133,7 +140,33 @@ function Account() { const [key, setKey] = useState(0) useLayoutEffect(() => { requestAnimationFrame(() => setKey((key) => key + 1)) - }, []) + }, [account?.address]) + + // Flash animation when account changes + const [isFlashing, setIsFlashing] = useState(false) + const [transitionDuration, setTransitionDuration] = useState('200ms') + const prevAccountRef = useRef(account?.address) + + useEffect(() => { + if ( + account?.address && + account.address !== prevAccountRef.current && + prevAccountRef.current !== undefined + ) { + // Fast switch to flash color (50ms) + setTransitionDuration('50ms') + setIsFlashing(true) + + // Then slowly fade back (200ms) + const fadeTimer = setTimeout(() => { + setTransitionDuration('200ms') + setIsFlashing(false) + }, 50) + + return () => clearTimeout(fadeTimer) + } + prevAccountRef.current = account?.address + }, [account?.address]) if (!account) return null return ( @@ -145,7 +178,13 @@ function Account() { }} display="flex" height="full" - style={{ cursor: 'default' }} + style={{ + cursor: 'default', + transition: `background-color ${transitionDuration} ease-out`, + ...(isFlashing && { + backgroundColor: 'var(--flash-color)', + }), + }} > {account && ( diff --git a/src/components/tabs/TabsList.tsx b/src/components/tabs/TabsList.tsx index da3873c..2371aaf 100644 --- a/src/components/tabs/TabsList.tsx +++ b/src/components/tabs/TabsList.tsx @@ -1,6 +1,6 @@ import * as Tabs_ from '@radix-ui/react-tabs' -import { Bleed, Box, Inline, Separator, Text } from '~/design-system' +import { Bleed, Box, Column, Columns, Separator, Text } from '~/design-system' import * as styles from './TabsList.css' @@ -16,27 +16,29 @@ export function TabsList({ items, onSelect }: TabsListProps) { <>
- + {items.map((item) => ( - - onSelect?.(item)} - style={{ height: '32px' }} + + - {item.label} - - + onSelect?.(item)} + style={{ height: '32px' }} + width="full" + > + {item.label} + + + ))} - +
diff --git a/src/design-system/components/Button.tsx b/src/design-system/components/Button.tsx index ccfe73f..4b8fb10 100644 --- a/src/design-system/components/Button.tsx +++ b/src/design-system/components/Button.tsx @@ -146,7 +146,7 @@ export const ButtonRoot = forwardRef( onClick={onClick as any} disabled={disabled} className={[buttonHeightStyles[height], className]} - cursor={disabled ? 'not-allowed' : undefined} + cursor={disabled ? 'not-allowed' : 'pointer'} display="flex" alignItems="center" justifyContent="center" diff --git a/src/design-system/components/Toggle.tsx b/src/design-system/components/Toggle.tsx new file mode 100644 index 0000000..9beb495 --- /dev/null +++ b/src/design-system/components/Toggle.tsx @@ -0,0 +1,46 @@ +import { Box } from './Box' + +type ToggleProps = { + checked: boolean + onChange: (checked: boolean) => void + disabled?: boolean +} + +export function Toggle({ checked, onChange, disabled }: ToggleProps) { + return ( + !disabled && onChange(!checked)} + disabled={disabled} + cursor={disabled ? 'not-allowed' : 'pointer'} + opacity={disabled ? '0.5' : undefined} + style={{ + position: 'relative', + width: '37px', + height: '20px', + borderRadius: '4px', + backgroundColor: checked + ? 'rgb(var(--toggle-active-bg))' + : 'rgb(var(--toggle-inactive-bg))', + border: 'none', + transition: 'background-color 200ms ease', + outline: 'none', + }} + > + + + ) +} diff --git a/src/design-system/index.ts b/src/design-system/index.ts index 617e1dd..90c5f2a 100644 --- a/src/design-system/index.ts +++ b/src/design-system/index.ts @@ -16,6 +16,7 @@ export { Separator } from './components/Separator' export { SFSymbol } from './components/SFSymbol' export { Stack } from './components/Stack' export { Text } from './components/Text' +export { Toggle } from './components/Toggle' export { initializeTheme } from './utils/initializeTheme' export { getTheme, setTheme } from './utils/theme' diff --git a/src/design-system/styles/theme.css.ts b/src/design-system/styles/theme.css.ts index b8df262..dda3ad4 100644 --- a/src/design-system/styles/theme.css.ts +++ b/src/design-system/styles/theme.css.ts @@ -100,6 +100,9 @@ globalStyle( [inheritedColorVars.accent]: toRgb( defaultInheritedColor.accent.light, ), + '--flash-color': '#2a2520', + '--toggle-active-bg': '74, 144, 226', + '--toggle-inactive-bg': '232, 229, 224', ...assignVars( backgroundColorVars, mapValues(backgroundColor, (color) => toRgb(color.light.value)), @@ -174,6 +177,9 @@ globalStyle( colorScheme: 'dark', vars: { [inheritedColorVars.accent]: toRgb(defaultInheritedColor.accent.dark), + '--flash-color': '#f9f7f4', + '--toggle-active-bg': '57, 255, 20', + '--toggle-inactive-bg': '68, 71, 75', ...assignVars( backgroundColorVars, mapValues(backgroundColor, (color) => toRgb(color.dark.value)), diff --git a/src/design-system/tokens.ts b/src/design-system/tokens.ts index 84aab5e..aa15015 100644 --- a/src/design-system/tokens.ts +++ b/src/design-system/tokens.ts @@ -20,7 +20,7 @@ export type BackgroundColorValue = Record< export const backgroundColor = { 'surface/primary': { light: { - value: '#f5f5f5', + value: '#f9f7f4', scheme: 'light', hover: { brightness: '0.98', @@ -36,7 +36,7 @@ export const backgroundColor = { }, 'surface/primary/elevated': { light: { - value: '#FFFFFF', + value: '#fdfcfb', scheme: 'light', hover: { brightness: '0.98', @@ -52,7 +52,7 @@ export const backgroundColor = { }, 'surface/secondary': { light: { - value: '#F0F1F5', + value: '#f3f2ee', scheme: 'light', hover: { brightness: '0.98', @@ -68,7 +68,7 @@ export const backgroundColor = { }, 'surface/secondary/elevated': { light: { - value: '#FFFFFF', + value: '#fdfcfb', scheme: 'light', hover: { brightness: '0.98', @@ -84,7 +84,7 @@ export const backgroundColor = { }, 'surface/fill': { light: { - value: '#D9DBDF', + value: '#e8e5e0', scheme: 'light', hover: { brightness: '0.98', @@ -100,7 +100,7 @@ export const backgroundColor = { }, 'surface/fill/secondary': { light: { - value: '#E4E6EA', + value: '#ede9e4', scheme: 'light', hover: { brightness: '0.98', @@ -116,7 +116,7 @@ export const backgroundColor = { }, 'surface/fill/tertiary': { light: { - value: '#ececec', + value: '#f5f3f0', scheme: 'light', hover: { brightness: '0.98', @@ -148,7 +148,7 @@ export const backgroundColor = { }, 'surface/fill/quarternary': { light: { - value: '#f9f9f9', + value: '#faf9f7', scheme: 'light', hover: { brightness: '0.98', @@ -352,45 +352,49 @@ export type BackgroundColor = keyof typeof backgroundColor export type ForegroundColorValue = Record export const foregroundColor = { 'separator/primary': { - light: '#000000', + light: '#3d3932', dark: '#ffffff', }, 'separator/secondary': { - light: '#202020', + light: '#5a5550', dark: '#9f9f9f', }, 'separator/tertiary': { - light: '#E4E5E9', + light: '#dcd9d4', dark: '#3C3F43', }, 'separator/quarternary': { - light: '#F1F2F6', + light: '#ebe8e3', dark: '#2B2C2F', }, 'stroke/primary': { - light: '#E4E5E9', + light: '#dcd9d4', dark: '#3C3F43', }, 'stroke/secondary': { - light: '#F1F2F6', + light: '#ebe8e3', dark: '#2B2C2F', }, 'text/primary': { - light: '#000000', + light: '#2a2520', dark: '#FFFFFF', }, 'text/secondary': { - light: '#5B5C5F', + light: '#5a5550', dark: '#C2C5CB', }, 'text/tertiary': { - light: '#848789', + light: '#7a7570', dark: '#9A9BA1', }, 'text/quarternary': { - light: '#A3A4A8', + light: '#9a9590', dark: '#78797E', }, + 'text/accent/active': { + light: '#4a90e2', + dark: '#39ff14', + }, } as const satisfies Record export type ForegroundColor = keyof typeof foregroundColor diff --git a/src/design-system/utils/initializeTheme.critical.ts b/src/design-system/utils/initializeTheme.critical.ts index a2909d1..258a08e 100644 --- a/src/design-system/utils/initializeTheme.critical.ts +++ b/src/design-system/utils/initializeTheme.critical.ts @@ -3,7 +3,7 @@ import { getTheme } from './theme' const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)') const { storageTheme, systemTheme } = getTheme() -const theme = storageTheme || systemTheme || 'dark' +const theme = storageTheme || systemTheme || 'light' document.documentElement.dataset.theme = theme diff --git a/src/entries/inpage/injectProvider.ts b/src/entries/inpage/injectProvider.ts index c941895..0646d3e 100644 --- a/src/entries/inpage/injectProvider.ts +++ b/src/entries/inpage/injectProvider.ts @@ -25,16 +25,23 @@ export function injectProvider() { requestMessenger: backgroundMessenger, }) - // Inject provider directly onto window - window.ethereum = provider - window.dispatchEvent(new Event('ethereum#initialized')) + // Only inject to window.ethereum if no other wallet exists + // This prevents conflicts with MetaMask, Coinbase, etc. + const hasExistingWallet = !!window.ethereum + if (!hasExistingWallet) { + window.ethereum = provider + window.dispatchEvent(new Event('ethereum#initialized')) + } - // Re-inject provider on demand + // Re-inject provider on demand (for compatibility) walletMessenger.reply('injectProvider', async () => { - window.ethereum = provider + if (!hasExistingWallet) { + window.ethereum = provider + } }) - // Announce provider + // Announce provider via EIP-6963 (modern multi-wallet support) + // This allows dapps to discover DevWallet even when other wallets are present announceProvider({ info: { icon: generateBrandIcon(), diff --git a/src/screens/index.tsx b/src/screens/index.tsx index 2c5ca4a..7edef09 100644 --- a/src/screens/index.tsx +++ b/src/screens/index.tsx @@ -78,9 +78,9 @@ export default function Index() { { setParams({ tab: item.value }) @@ -90,14 +90,14 @@ export default function Index() { - - + + - - + + @@ -146,68 +146,8 @@ function AccountRow({ account }: { account: Account }) { position="relative" style={{ height: '100px' }} > - {active && ( - - ACTIVE - - )} - {account.state === 'loading' && ( - - - - )} - - - - - {account.state === 'loading' && ( - - {truncate(account.key, { start: 20, end: 20 })} - - )} - {account.displayName && ( - {account.displayName} - )} - - - {truncatedAddress ?? account.address} - - - {account.address && ( - - - - )} - - - - - - - - - - - - {account.state === 'loaded' && ( - + {account.impersonate && ( )} {!active && ( - + { + e.stopPropagation() + setAccount({ account, setActive: true }) + }} + /> )} - + /> )} + {account.state === 'loading' && ( + + + + )} + + + + {account.state === 'loading' && ( + + {truncate(account.key, { start: 20, end: 20 })} + + )} + {account.displayName && ( + {account.displayName} + )} + + + {truncatedAddress ?? account.address} + + + {account.address && ( + + + + )} + + + + + + + + + + + + {active && ( + + ACTIVE + + )} ) } @@ -801,7 +802,7 @@ function Transactions() { @@ -955,7 +956,7 @@ function Contracts() { { + const { storageTheme, systemTheme } = getTheme() + return storageTheme || systemTheme || 'light' + }) + + const handleSetTheme = (theme: 'light' | 'dark') => { + setTheme(theme) + setCurrentTheme(theme) + } + return ( + Appearance + + + Theme + + + + + + + + + + Cheats @@ -23,18 +88,10 @@ export default function Settings() { alignHorizontal="justify" wrap={false} > - - Bypass Connect Authorization - - {/** TODO: component */} - { - setBypassConnectAuth(e.target.checked) - }} - type="checkbox" + Bypass Connect Authorization + - - Bypass Signature Authorization - - {/** TODO: component */} - { - setBypassSignatureAuth(e.target.checked) - }} - type="checkbox" + Bypass Signature Authorization + - - Bypass Transaction Authorization - - {/** TODO: component */} - { - setBypassTransactionAuth(e.target.checked) - }} - type="checkbox" + Bypass Transaction Authorization +