Skip to content

Commit

Permalink
Network Autocomplete (#1177)
Browse files Browse the repository at this point in the history
Co-authored-by: gregs <gregs@rainbow.me>
  • Loading branch information
brunobar79 and greg-schrammel authored Dec 6, 2023
1 parent 324c030 commit 2564a97
Show file tree
Hide file tree
Showing 5 changed files with 351 additions and 23 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
"check-password-strength": "2.0.7",
"chroma-js": "2.4.2",
"clsx": "1.2.1",
"cmdk": "0.2.0",
"currency.js": "2.0.4",
"date-fns": "2.29.3",
"deepmerge": "4.2.2",
Expand Down Expand Up @@ -310,4 +311,4 @@
"wagmi>@wagmi/core>@wagmi/connectors>@coinbase/wallet-sdk>keccak": false
}
}
}
}
155 changes: 155 additions & 0 deletions src/entries/popup/components/Autocomplete/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { Command } from 'cmdk';
import { forwardRef } from 'react';

import { Box } from '~/design-system';
import { stylesForHeight } from '~/design-system/components/Input/Input';
import { BoxStyles, semanticColorVars } from '~/design-system/styles/core.css';

export interface AutocompleteItem {
name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value?: any;
}

export interface AutocompleteData {
[key: string]: AutocompleteItem[];
}

export interface AutocompleteProps {
data: AutocompleteData;
value: string;
onChange: (value: string) => void;
onSelect: (value: string) => void;
onBlur: () => void;
onFocus: () => void;
borderColor?: BoxStyles['borderColor'];
placeholder?: string;
open: boolean;
}

export const Autocomplete = forwardRef<HTMLInputElement, AutocompleteProps>(
(
{
data,
value,
onChange,
onSelect,
onBlur,
onFocus,
borderColor,
placeholder,
open,
}: AutocompleteProps,
ref,
) => {
const {
borderRadius: defaultBorderRadius,
fontSize: defaultFontSize,
paddingHorizontal,
paddingVertical,
} = stylesForHeight['32px'];

return (
<Box position="relative">
<Command label="">
<Box
borderColor={borderColor as BoxStyles['borderColor']}
borderRadius={defaultBorderRadius}
borderWidth="1px"
>
<Command.Input
value={value}
onValueChange={onChange}
style={{
width: '100%',
fontSize: defaultFontSize?.replace('pt', 'px'),
paddingLeft: paddingHorizontal,
paddingRight: paddingHorizontal,
paddingTop: paddingVertical,
paddingBottom: paddingVertical,

outline: 'none',
backgroundColor:
semanticColorVars.backgroundColors.surfacePrimaryElevated,
color: semanticColorVars.foregroundColors.label,
height: '32px',
overflow: 'hidden',
borderRadius: defaultBorderRadius,
border: 'none',
}}
placeholder={placeholder}
// otherwise blur triggers before onSelect
onBlur={() => setTimeout(onBlur, 200)}
onFocus={onFocus}
ref={ref}
/>
</Box>
{open && (
<Box
borderColor={'accent'}
background="surfacePrimaryElevated"
style={{
position: 'absolute',
zIndex: 999999,
width: '100%',
boxShadow: '2px 12px 12px rgba(0, 0, 0, 0.65)',
borderRadius: defaultBorderRadius,
}}
>
<Command.List>
{Object.keys(data).map((key) => {
return (
<Command.Group
heading={key}
key={key}
style={{
color:
semanticColorVars.foregroundColors.labelSecondary,
fontSize: '12px',
padding: '20px 18px',
}}
>
<Box paddingTop="12px">
{data[key].map((item: { name: string }) => (
<Box key={`${key}_${item.name}`}>
<Command.Item
style={{
padding: '8px 12px',
fontSize: '14px',
lineHeight: '20px',
borderRadius: '8px',
outline: 'none',
backgroundColor:
semanticColorVars.backgroundColors
.surfacePrimaryElevated,
color: semanticColorVars.foregroundColors.label,
}}
onSelect={() => onSelect(item.name)}
value={item.name}
onMouseOver={(e) => {
e.currentTarget.style.backgroundColor =
semanticColorVars.backgroundColors.surfaceSecondaryElevated;
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor =
semanticColorVars.backgroundColors.surfacePrimaryElevated;
}}
>
{item.name}
</Command.Item>
</Box>
))}
</Box>
</Command.Group>
);
})}
</Command.List>
</Box>
)}
</Command>
</Box>
);
},
);

Autocomplete.displayName = 'Autocomplete';
106 changes: 85 additions & 21 deletions src/entries/popup/pages/settings/customChain/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { isEqual } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Chain } from 'wagmi';

import { i18n } from '~/core/languages';
import { useChainMetadata } from '~/core/resources/chains/chainMetadata';
import { useCustomRPCsStore } from '~/core/state/customRPC';
import { useUserChainsStore } from '~/core/state/userChains';
import { isValidUrl } from '~/core/utils/connectedApps';
import { Box, Button, Inline, Stack, Text } from '~/design-system';
import { Autocomplete } from '~/entries/popup/components/Autocomplete';
import { Form } from '~/entries/popup/components/Form/Form';
import { FormInput } from '~/entries/popup/components/Form/FormInput';
import { useDebounce } from '~/entries/popup/hooks/useDebounce';
Expand All @@ -17,10 +19,36 @@ import { ROUTES } from '~/entries/popup/urls';
import { Checkbox } from '../../../components/Checkbox/Checkbox';
import { maskInput } from '../../../components/InputMask/utils';

const KNOWN_NETWORKS = {
[i18n.t('settings.networks.custom_rpc.networks')]: [
{
name: 'Avalanche',
value: {
rpcUrl: 'https://api.avax.network/ext/bc/C/rpc',
chainId: 43114,
decimals: 18,
symbol: 'AVAX',
explorerUrl: 'https://cchain.explorer.avax.network',
},
},
{
name: 'PulseChain',
value: {
rpcUrl: 'https://rpc.pulsechain.com',
chainId: 369,
decimals: 18,
symbol: 'PULSE',
explorerUrl: 'https://pulsechain.com',
},
},
],
};

export function SettingsCustomChain() {
const navigate = useRainbowNavigate();
const { customChains, addCustomRPC } = useCustomRPCsStore();
const { addUserChain } = useUserChainsStore();
const [open, setOpen] = useState(false);
const [customRPC, setCustomRPC] = useState<{
active?: boolean;
rpcUrl?: string;
Expand All @@ -43,6 +71,7 @@ export function SettingsCustomChain() {
explorerUrl: true,
});
const debuncedRpcUrl = useDebounce(customRPC.rpcUrl, 500);
const inputRef = useRef<HTMLInputElement>(null);

const {
data: chainMetadata,
Expand Down Expand Up @@ -101,12 +130,14 @@ export function SettingsCustomChain() {
[validateChainId],
);

const validateName = useCallback(() => !!customRPC.name, [customRPC.name]);
const validateName = useCallback(() => {
return !!inputRef.current?.value;
}, []);

const onNameBlur = useCallback(
() => setValidations((prev) => ({ ...prev, name: validateName() })),
[validateName],
);
const onNameBlur = useCallback(() => {
setValidations((prev) => ({ ...prev, name: validateName() }));
open && setOpen(false);
}, [open, validateName]);

const validateSymbol = useCallback(
() => !!customRPC.symbol,
Expand Down Expand Up @@ -216,6 +247,34 @@ export function SettingsCustomChain() {
validateCustomRpcMetadata,
]);

const handleNetworkSelect = useCallback(
(networkName: string) => {
const network = KNOWN_NETWORKS.Networks.find(
(network) => network.name === networkName,
);
if (network) {
setCustomRPC((prev) => ({
...prev,
...network.value,
name: networkName,
active: true,
}));

// All these are previously validated by us
// when adding the network to the list
setValidations({
rpcUrl: true,
chainId: true,
name: true,
symbol: true,
explorerUrl: true,
});
}
open && setOpen(false);
},
[open],
);

return (
<Box paddingHorizontal="20px">
<Stack space="20px">
Expand Down Expand Up @@ -261,11 +320,23 @@ export function SettingsCustomChain() {
))}

<Form>
<Autocomplete
open={open}
onFocus={() => setOpen(true)}
onBlur={onNameBlur}
data={KNOWN_NETWORKS}
value={customRPC.name || ''}
borderColor={validations.name ? 'accent' : 'red'}
placeholder={i18n.t('settings.networks.custom_rpc.network_name')}
onChange={(value) => onInputChange<string>(value, 'string', 'name')}
onSelect={handleNetworkSelect}
ref={inputRef}
/>
<FormInput
onChange={(t) =>
onInputChange<string>(t.target.value, 'string', 'rpcUrl')
}
placeholder="Url"
placeholder={i18n.t('settings.networks.custom_rpc.rpc_url')}
value={customRPC.rpcUrl}
onBlur={onRpcUrlBlur}
borderColor={
Expand All @@ -277,25 +348,16 @@ export function SettingsCustomChain() {
onChange={(t) =>
onInputChange<number>(t.target.value, 'number', 'chainId')
}
placeholder="ChainId"
placeholder={i18n.t('settings.networks.custom_rpc.chain_id')}
value={customRPC.chainId || chainMetadata?.chainId || ''}
onBlur={onChainIdBlur}
borderColor={validations.chainId ? 'accent' : 'red'}
/>
<FormInput
onChange={(t) =>
onInputChange<string>(t.target.value, 'string', 'name')
}
placeholder="name"
value={customRPC.name}
onBlur={onNameBlur}
borderColor={validations.name ? 'accent' : 'red'}
/>
<FormInput
onChange={(t) =>
onInputChange<string>(t.target.value, 'string', 'symbol')
}
placeholder="Symbol"
placeholder={i18n.t('settings.networks.custom_rpc.symbol')}
value={customRPC.symbol}
onBlur={onSymbolBlur}
borderColor={validations.symbol ? 'accent' : 'red'}
Expand All @@ -304,7 +366,9 @@ export function SettingsCustomChain() {
onChange={(t) =>
onInputChange<string>(t.target.value, 'string', 'explorerUrl')
}
placeholder="Explorer url"
placeholder={i18n.t(
'settings.networks.custom_rpc.block_explorer_url',
)}
value={customRPC.explorerUrl}
onBlur={onExplorerUrlBlur}
borderColor={validations.explorerUrl ? 'accent' : 'red'}
Expand All @@ -317,7 +381,7 @@ export function SettingsCustomChain() {
size="12pt"
color="labelSecondary"
>
{'Active'}
{i18n.t('settings.networks.custom_rpc.active')}
</Text>
<Checkbox
borderColor="accent"
Expand All @@ -335,7 +399,7 @@ export function SettingsCustomChain() {
height="36px"
variant="raised"
>
Add
{i18n.t('settings.networks.custom_rpc.add_network')}
</Button>
</Inline>
</Form>
Expand Down
11 changes: 10 additions & 1 deletion static/json/languages/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,16 @@
"toggle_explainer": "Enables the Testnet Mode shortcut (T) and adds a toggle to the home screen’s top-right menu."
},
"custom_rpc": {
"title": "Custom RPC"
"title": "Custom RPC",
"networks": "Networks",
"network_name": "Network Name",
"rpc_url": "RPC URL",
"chain_id": "Chain ID",
"symbol": "Symbol",
"decimals": "Decimals",
"active": "Active",
"block_explorer_url": "Block Explorer URL",
"add_network": "Add Network"
}
},
"privacy_and_security": {
Expand Down
Loading

0 comments on commit 2564a97

Please sign in to comment.