From 7b9ec424cc6c3e7c508d31362f020186faa34676 Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Mon, 1 Apr 2024 12:39:27 -0700 Subject: [PATCH 01/20] Create initial component for setting RPC endpoint --- .../extension/src/routes/page/onboarding/routes.tsx | 13 +++++++++---- .../src/routes/page/onboarding/set-rpc-endpoint.tsx | 12 ++++++++++++ apps/extension/src/routes/page/paths.ts | 1 + 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx diff --git a/apps/extension/src/routes/page/onboarding/routes.tsx b/apps/extension/src/routes/page/onboarding/routes.tsx index 87f5186d70..f42ffc7f62 100644 --- a/apps/extension/src/routes/page/onboarding/routes.tsx +++ b/apps/extension/src/routes/page/onboarding/routes.tsx @@ -6,6 +6,7 @@ import { ImportSeedPhrase } from './import'; import { OnboardingSuccess } from './success'; import { SetPassword } from './set-password'; import { pageIndexLoader } from '..'; +import { SetRpcEndpoint } from './set-rpc-endpoint'; export const onboardingRoutes = [ { @@ -24,13 +25,17 @@ export const onboardingRoutes = [ path: PagePath.IMPORT_SEED_PHRASE, element: , }, + { + path: PagePath.SET_PASSWORD, + element: , + }, + { + path: PagePath.SET_RPC_ENDPOINT, + element: , + }, { path: PagePath.ONBOARDING_SUCCESS, element: , loader: pageIndexLoader, }, - { - path: PagePath.SET_PASSWORD, - element: , - }, ]; diff --git a/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx b/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx new file mode 100644 index 0000000000..f9109ddff6 --- /dev/null +++ b/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx @@ -0,0 +1,12 @@ +import { Card } from '@penumbra-zone/ui/components/ui/card'; +import { FadeTransition } from '@penumbra-zone/ui/components/ui/fade-transition'; + +export const SetRpcEndpoint = () => { + return ( + + +
Hello, world!
+
+
+ ); +}; diff --git a/apps/extension/src/routes/page/paths.ts b/apps/extension/src/routes/page/paths.ts index c5a9be2e3b..b0cf73bf03 100644 --- a/apps/extension/src/routes/page/paths.ts +++ b/apps/extension/src/routes/page/paths.ts @@ -6,6 +6,7 @@ export enum PagePath { IMPORT_SEED_PHRASE = '/welcome/import', ONBOARDING_SUCCESS = '/welcome/success', SET_PASSWORD = '/welcome/set-password', + SET_RPC_ENDPOINT = '/welcome/set-rpc-endpoint', RESTORE_PASSWORD = '/restore-password', RESTORE_PASSWORD_INDEX = '/restore-password/', RESTORE_PASSWORD_SET_PASSWORD = '/restore-password/set-password', From d3fa1d06016c0ad1d53995cd26df5cc80508b7a7 Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Mon, 1 Apr 2024 16:47:00 -0700 Subject: [PATCH 02/20] Create RPC_ENDPOINTS const --- apps/extension/src/shared/rpc-endpoints.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 apps/extension/src/shared/rpc-endpoints.ts diff --git a/apps/extension/src/shared/rpc-endpoints.ts b/apps/extension/src/shared/rpc-endpoints.ts new file mode 100644 index 0000000000..aed7e50e6d --- /dev/null +++ b/apps/extension/src/shared/rpc-endpoints.ts @@ -0,0 +1,16 @@ +interface RpcEndpoint { + name: string; + url: string; + imageUrl?: string; +} + +export const RPC_ENDPOINTS: RpcEndpoint[] = [ + { + name: 'Penumbra Labs Testnet RPC', + url: 'https://grpc.testnet.penumbra.zone', + }, + { + name: 'Penumbra Labs Testnet Preview RPC', + url: 'https://grpc.testnet-preview.penumbra.zone', + }, +]; From 8674da367e713a93088049f5c68c2215725c22cb Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Mon, 1 Apr 2024 17:47:53 -0700 Subject: [PATCH 03/20] Create SelectList component --- .../ui/components/ui/select-list.stories.tsx | 43 +++++++++++++++++++ packages/ui/components/ui/select-list.tsx | 41 ++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 packages/ui/components/ui/select-list.stories.tsx create mode 100644 packages/ui/components/ui/select-list.tsx diff --git a/packages/ui/components/ui/select-list.stories.tsx b/packages/ui/components/ui/select-list.stories.tsx new file mode 100644 index 0000000000..2a91ddaf69 --- /dev/null +++ b/packages/ui/components/ui/select-list.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { useArgs } from '@storybook/preview-api'; + +import { SelectList } from './select-list'; +import { ComponentProps } from 'react'; + +const meta: Meta = { + component: SelectList, + title: 'SelectList', + tags: ['autodocs'], +}; +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + render: function Render() { + const [args, updateArgs] = useArgs>(); + + const onChange = (value: unknown) => updateArgs({ value }); + + const options = [ + { value: 'one', label: 'One', secondaryText: 'AKA, 1' }, + { value: 'two', label: 'Two', secondaryText: 'AKA, 2' }, + { value: 'three', label: 'Three', secondaryText: 'AKA, 3' }, + ]; + + return ( + + {options.map(option => ( + + ))} + + ); + }, +}; diff --git a/packages/ui/components/ui/select-list.tsx b/packages/ui/components/ui/select-list.tsx new file mode 100644 index 0000000000..28335b0244 --- /dev/null +++ b/packages/ui/components/ui/select-list.tsx @@ -0,0 +1,41 @@ +import { ReactNode } from 'react'; +import { cn } from '../../lib/utils'; + +/** + * A select list is a nicely formatted vertical list of options for a user to + * choose from. It's functionally identical to a series of radio buttons, but + * presents options as clickable rectangular buttons, rather than circular radio + * buttons. + */ +export const SelectList = ({ children }: { children: ReactNode }) => ( +
{children}
+); + +const Option = ({ + label, + secondaryText, + value, + onSelect, + isSelected, +}: { + label: string; + secondaryText?: string; + value: T; + onSelect: (value: T) => void; + isSelected: boolean; +}) => ( +
onSelect(value)} + > + {label} + + {!!secondaryText && {secondaryText}} +
+); + +SelectList.Option = Option; From 0d653d17bbdd0e0fc671e30fc9bb545b257b7eb0 Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Mon, 1 Apr 2024 17:49:14 -0700 Subject: [PATCH 04/20] Build out RPC UI --- .../page/onboarding/set-rpc-endpoint.tsx | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx b/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx index f9109ddff6..a06ddbbd37 100644 --- a/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx +++ b/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx @@ -1,11 +1,37 @@ -import { Card } from '@penumbra-zone/ui/components/ui/card'; +import { Card, CardDescription, CardHeader, CardTitle } from '@penumbra-zone/ui/components/ui/card'; import { FadeTransition } from '@penumbra-zone/ui/components/ui/fade-transition'; +import { RPC_ENDPOINTS } from '../../../shared/rpc-endpoints'; +import { useMemo, useState } from 'react'; +import { SelectList } from '@penumbra-zone/ui/components/ui/select-list'; + +const randomSort = () => (Math.random() >= 0.5 ? 1 : -1); export const SetRpcEndpoint = () => { + const randomlySortedEndpoints = useMemo(() => [...RPC_ENDPOINTS].sort(randomSort), []); + const [selectedEndpointUrl, setSelectedEndpointUrl] = useState(randomlySortedEndpoints[0]?.url); + return ( -
Hello, world!
+ + Select your preferred RPC endpoint + + If you're unsure which one to choose, leave this option set to the default. + + + + + {randomlySortedEndpoints.map(rpcEndpoint => ( + + ))} +
); From 4b6e697139cfe2d1ed90b06f2e2e292b7c6aaa66 Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Mon, 1 Apr 2024 17:59:19 -0700 Subject: [PATCH 05/20] Add submit button and link to edit list --- .../page/onboarding/set-rpc-endpoint.tsx | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx b/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx index a06ddbbd37..58e2c9748c 100644 --- a/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx +++ b/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx @@ -3,6 +3,7 @@ import { FadeTransition } from '@penumbra-zone/ui/components/ui/fade-transition' import { RPC_ENDPOINTS } from '../../../shared/rpc-endpoints'; import { useMemo, useState } from 'react'; import { SelectList } from '@penumbra-zone/ui/components/ui/select-list'; +import { Button } from '@penumbra-zone/ui/components/ui/button'; const randomSort = () => (Math.random() >= 0.5 ? 1 : -1); @@ -10,6 +11,8 @@ export const SetRpcEndpoint = () => { const randomlySortedEndpoints = useMemo(() => [...RPC_ENDPOINTS].sort(randomSort), []); const [selectedEndpointUrl, setSelectedEndpointUrl] = useState(randomlySortedEndpoints[0]?.url); + const handleSubmit = () => {}; + return ( @@ -20,18 +23,33 @@ export const SetRpcEndpoint = () => { - - {randomlySortedEndpoints.map(rpcEndpoint => ( - - ))} - +
+ + {randomlySortedEndpoints.map(rpcEndpoint => ( + + ))} + + + +
+ + + Add to this list +
); From ab697d84ae9b6ed6d0b5b93f844d0176d064a9cb Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Mon, 1 Apr 2024 20:47:38 -0700 Subject: [PATCH 06/20] Use the RPC endpoint in state --- .../routes/page/onboarding/set-password.tsx | 6 ++-- .../page/onboarding/set-rpc-endpoint.tsx | 32 +++++++++++++++---- apps/extension/src/utils/use-store-shallow.ts | 27 ++++++++++++++++ 3 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 apps/extension/src/utils/use-store-shallow.ts diff --git a/apps/extension/src/routes/page/onboarding/set-password.tsx b/apps/extension/src/routes/page/onboarding/set-password.tsx index 04b21385d3..c755c17515 100644 --- a/apps/extension/src/routes/page/onboarding/set-password.tsx +++ b/apps/extension/src/routes/page/onboarding/set-password.tsx @@ -16,7 +16,7 @@ import { PasswordInput } from '../../../shared/components/password-input'; export const SetPassword = () => { const navigate = usePageNav(); - const finalOnboardingSave = useOnboardingSave(); + const onboardingSave = useOnboardingSave(); const [password, setPassword] = useState(''); const [confirmation, setConfirmation] = useState(''); @@ -24,8 +24,8 @@ export const SetPassword = () => { event.preventDefault(); void (async () => { - await finalOnboardingSave(password); - navigate(PagePath.ONBOARDING_SUCCESS); + await onboardingSave(password); + navigate(PagePath.SET_RPC_ENDPOINT); })(); }; diff --git a/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx b/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx index 58e2c9748c..3bb5208192 100644 --- a/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx +++ b/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx @@ -1,17 +1,35 @@ import { Card, CardDescription, CardHeader, CardTitle } from '@penumbra-zone/ui/components/ui/card'; import { FadeTransition } from '@penumbra-zone/ui/components/ui/fade-transition'; import { RPC_ENDPOINTS } from '../../../shared/rpc-endpoints'; -import { useMemo, useState } from 'react'; +import { FormEvent, useEffect, useMemo } from 'react'; import { SelectList } from '@penumbra-zone/ui/components/ui/select-list'; import { Button } from '@penumbra-zone/ui/components/ui/button'; +import { AllSlices } from '../../../state'; +import { useStoreShallow } from '../../../utils/use-store-shallow'; +import { usePageNav } from '../../../utils/navigate'; +import { PagePath } from '../paths'; const randomSort = () => (Math.random() >= 0.5 ? 1 : -1); +const setRpcEndpointSelector = (state: AllSlices) => ({ + grpcEndpoint: state.network.grpcEndpoint, + setGRPCEndpoint: state.network.setGRPCEndpoint, +}); + export const SetRpcEndpoint = () => { + const navigate = usePageNav(); const randomlySortedEndpoints = useMemo(() => [...RPC_ENDPOINTS].sort(randomSort), []); - const [selectedEndpointUrl, setSelectedEndpointUrl] = useState(randomlySortedEndpoints[0]?.url); + const { grpcEndpoint, setGRPCEndpoint } = useStoreShallow(setRpcEndpointSelector); + + useEffect( + () => void setGRPCEndpoint(randomlySortedEndpoints[0]!.url), + [randomlySortedEndpoints, setGRPCEndpoint], + ); - const handleSubmit = () => {}; + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + navigate(PagePath.ONBOARDING_SUCCESS); + }; return ( @@ -23,21 +41,21 @@ export const SetRpcEndpoint = () => { -
+ {randomlySortedEndpoints.map(rpcEndpoint => ( void setGRPCEndpoint(value)} value={rpcEndpoint.url} - isSelected={rpcEndpoint.url === selectedEndpointUrl} + isSelected={rpcEndpoint.url === grpcEndpoint} /> ))} -
diff --git a/apps/extension/src/utils/use-store-shallow.ts b/apps/extension/src/utils/use-store-shallow.ts new file mode 100644 index 0000000000..b364303382 --- /dev/null +++ b/apps/extension/src/utils/use-store-shallow.ts @@ -0,0 +1,27 @@ +import { useShallow } from 'zustand/react/shallow'; +import { AllSlices, useStore } from '../state'; + +/** + * Like `useStore()`, but checks for shallow equality to prevent unnecessary + * re-renders if none of the properties returned by `selector` have changed. + * + * Calling `useStoreShallow(selector)` is the same as calling + * `useStore(useShallow(selector))`. But it's so common to use those two + * together that this function combines both for ease of use. + * + * @example + * ```tsx + * import { useStoreShallow } from '../utils/use-store-shallow'; + * + * const myComponentSelector = (state: AllSlices) => ({ + * prop1: state.mySlice.prop1, + * prop2: state.mySlice.prop2, + * }); + * + * const MyComponent = () => { + * const state = useStoreShallow(myComponentSelector); + * }; + * ``` + */ +export const useStoreShallow = (selector: (state: AllSlices) => U) => + useStore(useShallow(selector)); From 2c7d979d39ae90ec3b4728014c4c7da33a941f19 Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Mon, 1 Apr 2024 21:00:19 -0700 Subject: [PATCH 07/20] Send the OnboardComplete message after selecting an RPC endpoint --- apps/extension/src/hooks/onboarding.ts | 2 -- apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/extension/src/hooks/onboarding.ts b/apps/extension/src/hooks/onboarding.ts index f6adc9979d..1308d7e0e4 100644 --- a/apps/extension/src/hooks/onboarding.ts +++ b/apps/extension/src/hooks/onboarding.ts @@ -3,7 +3,6 @@ import { passwordSelector } from '../state/password'; import { generateSelector } from '../state/seed-phrase/generate'; import { importSelector } from '../state/seed-phrase/import'; import { walletsSelector } from '../state/wallets'; -import { ServicesMessage } from '@penumbra-zone/types/src/services'; // Saves hashed password, uses that hash to encrypt the seed phrase // and then saves that to session + local storage @@ -19,6 +18,5 @@ export const useOnboardingSave = () => { await setPassword(plaintextPassword); await addWallet({ label: 'Wallet #1', seedPhrase }); - void chrome.runtime.sendMessage(ServicesMessage.OnboardComplete); }; }; diff --git a/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx b/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx index 3bb5208192..4db8e50f34 100644 --- a/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx +++ b/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx @@ -8,6 +8,7 @@ import { AllSlices } from '../../../state'; import { useStoreShallow } from '../../../utils/use-store-shallow'; import { usePageNav } from '../../../utils/navigate'; import { PagePath } from '../paths'; +import { ServicesMessage } from '@penumbra-zone/types/src/services'; const randomSort = () => (Math.random() >= 0.5 ? 1 : -1); @@ -28,6 +29,7 @@ export const SetRpcEndpoint = () => { const handleSubmit = (e: FormEvent) => { e.preventDefault(); + void chrome.runtime.sendMessage(ServicesMessage.OnboardComplete); navigate(PagePath.ONBOARDING_SUCCESS); }; From d4c0d4dec3acd6ad8f23ab6f1464de5374ead873 Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Mon, 1 Apr 2024 21:34:27 -0700 Subject: [PATCH 08/20] Add a custom RPC endpoint option --- .../page/onboarding/set-rpc-endpoint.tsx | 23 ++++++++++++++++++- packages/ui/components/ui/select-list.tsx | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx b/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx index 4db8e50f34..5c628a7f67 100644 --- a/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx +++ b/apps/extension/src/routes/page/onboarding/set-rpc-endpoint.tsx @@ -1,7 +1,7 @@ import { Card, CardDescription, CardHeader, CardTitle } from '@penumbra-zone/ui/components/ui/card'; import { FadeTransition } from '@penumbra-zone/ui/components/ui/fade-transition'; import { RPC_ENDPOINTS } from '../../../shared/rpc-endpoints'; -import { FormEvent, useEffect, useMemo } from 'react'; +import { FormEvent, useEffect, useMemo, useRef } from 'react'; import { SelectList } from '@penumbra-zone/ui/components/ui/select-list'; import { Button } from '@penumbra-zone/ui/components/ui/button'; import { AllSlices } from '../../../state'; @@ -21,6 +21,8 @@ export const SetRpcEndpoint = () => { const navigate = usePageNav(); const randomlySortedEndpoints = useMemo(() => [...RPC_ENDPOINTS].sort(randomSort), []); const { grpcEndpoint, setGRPCEndpoint } = useStoreShallow(setRpcEndpointSelector); + const customRpcEndpointInput = useRef(null); + const isCustomRpcEndpoint = !RPC_ENDPOINTS.some(({ url }) => url === grpcEndpoint); useEffect( () => void setGRPCEndpoint(randomlySortedEndpoints[0]!.url), @@ -55,6 +57,25 @@ export const SetRpcEndpoint = () => { isSelected={rpcEndpoint.url === grpcEndpoint} /> ))} + + void setGRPCEndpoint(e.target.value)} + className='w-full bg-transparent' + /> + } + onSelect={() => { + if (!isCustomRpcEndpoint) void setGRPCEndpoint(''); + customRpcEndpointInput.current?.focus(); + }} + isSelected={isCustomRpcEndpoint} + value={''} + />