This repository was archived by the owner on Jan 22, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 939
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
An example React app that exercises the new
@solana/web3.js
and the…
… new Wallet Standard wallet adapter in `@solana/react`
1 parent
1b73e0f
commit ca58fa4
Showing
37 changed files
with
4,346 additions
and
851 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
module.exports = { | ||
root: true, | ||
env: { browser: true, es2020: true }, | ||
extends: ['../../.eslintrc.js', '@solana/eslint-config-solana/react'], | ||
ignorePatterns: ['dist', '.eslintrc.cjs'], | ||
plugins: ['react-refresh'], | ||
rules: { | ||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
pnpm-debug.log* | ||
lerna-debug.log* | ||
|
||
node_modules | ||
dist | ||
dist-ssr | ||
*.local | ||
|
||
# Editor directories and files | ||
.vscode/* | ||
!.vscode/extensions.json | ||
.idea | ||
.DS_Store | ||
*.suo | ||
*.ntvs* | ||
*.njsproj | ||
*.sln | ||
*.sw? |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
Copyright (c) 2023 Solana Labs, Inc | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining | ||
a copy of this software and associated documentation files (the | ||
"Software"), to deal in the Software without restriction, including | ||
without limitation the rights to use, copy, modify, merge, publish, | ||
distribute, sublicense, and/or sell copies of the Software, and to | ||
permit persons to whom the Software is furnished to do so, subject to | ||
the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be | ||
included in all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# @solana/react-example-app |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<link rel="icon" type="image/svg+xml" href="/solanaLogoMark.svg" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Solana React Example App</title> | ||
</head> | ||
<body> | ||
<div id="root"></div> | ||
<script type="module" src="/src/main.tsx"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
{ | ||
"name": "@solana/example-react-appz", | ||
"private": true, | ||
"version": "0.0.0", | ||
"type": "module", | ||
"scripts": { | ||
"dev": "vite", | ||
"build": "tsc && vite build", | ||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", | ||
"preview": "vite preview" | ||
}, | ||
"dependencies": { | ||
"@radix-ui/react-dropdown-menu": "^2.0.6", | ||
"@radix-ui/react-icons": "^1.3.0", | ||
"@radix-ui/themes": "^3.0.5", | ||
"@solana-program/system": "^0.3.2", | ||
"@solana/react": "workspace:*", | ||
"@solana/web3.js": "workspace:@solana/web3.js-experimental@*", | ||
"@wallet-standard/core": "pre", | ||
"@wallet-standard/react": "pre", | ||
"react": "^18.3.0", | ||
"react-dom": "^18.3.0", | ||
"react-error-boundary": "^4.0.13", | ||
"swr": "^2.2.5" | ||
}, | ||
"devDependencies": { | ||
"@solana/wallet-standard-features": "^1.2.0", | ||
"@types/react": "^18.3", | ||
"@types/react-dom": "^18.3", | ||
"@typescript-eslint/eslint-plugin": "^7.2.0", | ||
"@typescript-eslint/parser": "^7.2.0", | ||
"@vitejs/plugin-react-swc": "^3.5.0", | ||
"eslint": "^8.57.0", | ||
"eslint-plugin-react-hooks": "^4.6.0", | ||
"eslint-plugin-react-refresh": "^0.4.6", | ||
"typescript": "^5.2.2", | ||
"vite": "^5.2.0" | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { Text } from '@radix-ui/themes'; | ||
import { AccountNotificationsApi, Address, address, GetBalanceApi, Rpc, RpcSubscriptions } from '@solana/web3.js'; | ||
import type { UiWalletAccount } from '@wallet-standard/react'; | ||
import { useCallback, useContext } from 'react'; | ||
import useSWRSubscription, { SWRSubscription } from 'swr/subscription'; | ||
|
||
import { ChainContext } from '../context/ChainContext'; | ||
import { RpcContext } from '../context/RpcContext'; | ||
|
||
type Props = Readonly<{ | ||
account: UiWalletAccount; | ||
}>; | ||
|
||
function balanceSubscribe( | ||
rpc: Rpc<GetBalanceApi>, | ||
rpcSubscriptions: RpcSubscriptions<AccountNotificationsApi>, | ||
{ address }: { address: Address }, | ||
{ next }: { next(error: unknown, value?: bigint): void }, | ||
) { | ||
const abortController = new AbortController(); | ||
let lastUpdateSlot = 0n; | ||
// Fetch the current balance of this account. | ||
rpc.getBalance(address, { commitment: 'confirmed' }) | ||
.send({ abortSignal: abortController.signal }) | ||
.then(({ context: { slot }, value: lamports }) => { | ||
if (slot < lastUpdateSlot) { | ||
// This implies that we already received an update newer than this one (ie. from the | ||
// subscription below). | ||
return; | ||
} | ||
lastUpdateSlot = slot; | ||
next(null, lamports); | ||
}) | ||
.catch(e => next(e)); | ||
// Subscribe for updates to that balance. | ||
rpcSubscriptions | ||
.accountNotifications(address) | ||
.subscribe({ abortSignal: abortController.signal }) | ||
.then(async accountInfoNotifications => { | ||
try { | ||
for await (const { | ||
context: { slot }, | ||
value, | ||
} of accountInfoNotifications) { | ||
if (slot < lastUpdateSlot) { | ||
// This implies that we already received an update newer than this one (ie. | ||
// from the fetch above). | ||
continue; | ||
} | ||
lastUpdateSlot = slot; | ||
next(null, value.lamports); | ||
} | ||
} catch (e) { | ||
next(e); | ||
} | ||
}) | ||
.catch(e => next(e)); | ||
return () => abortController.abort(); | ||
} | ||
|
||
export function Balance({ account }: Props) { | ||
const { chain } = useContext(ChainContext); | ||
const { rpc, rpcSubscriptions } = useContext(RpcContext); | ||
const subscribe = useCallback<SWRSubscription<{ address: Address }, bigint>>( | ||
(...args) => balanceSubscribe(rpc, rpcSubscriptions, ...args), | ||
[rpc, rpcSubscriptions], | ||
); | ||
const { data: lamports } = useSWRSubscription({ address: address(account.address), chain }, subscribe); | ||
if (lamports == null) { | ||
return <Text>–</Text>; | ||
} else { | ||
const formattedSolValue = new Intl.NumberFormat(undefined, { maximumFractionDigits: 5 }).format( | ||
// @ts-expect-error This format string is 100% allowed now. | ||
`${lamports}E-9`, | ||
); | ||
return <Text>{`${formattedSolValue} \u25CE`}</Text>; | ||
} | ||
} |
100 changes: 100 additions & 0 deletions
100
examples/react-app/src/components/BaseSignMessageFeaturePanel.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { Pencil1Icon } from '@radix-ui/react-icons'; | ||
import { Blockquote, Box, Button, Code, DataList, Dialog, Flex, TextField } from '@radix-ui/themes'; | ||
import { getBase64Decoder } from '@solana/web3.js'; | ||
import type { ReadonlyUint8Array } from '@wallet-standard/core'; | ||
import type { SyntheticEvent } from 'react'; | ||
import { useRef, useState } from 'react'; | ||
|
||
import { ErrorDialog } from '../components/ErrorDialog'; | ||
|
||
type Props = Readonly<{ | ||
signMessage(message: ReadonlyUint8Array): Promise<ReadonlyUint8Array>; | ||
}>; | ||
|
||
export function BaseSignMessageFeaturePanel({ signMessage }: Props) { | ||
const { current: NO_ERROR } = useRef(Symbol()); | ||
const [isSigningMessage, setIsSigningMessage] = useState(false); | ||
const [error, setError] = useState<unknown | typeof NO_ERROR>(NO_ERROR); | ||
const [lastSignature, setLastSignature] = useState<ReadonlyUint8Array | undefined>(); | ||
const [text, setText] = useState<string>(); | ||
return ( | ||
<Flex asChild gap="2" direction={{ initial: 'column', sm: 'row' }} style={{ width: '100%' }}> | ||
<form | ||
onSubmit={async e => { | ||
e.preventDefault(); | ||
setError(NO_ERROR); | ||
setIsSigningMessage(true); | ||
try { | ||
const signature = await signMessage(new TextEncoder().encode(text)); | ||
setLastSignature(signature); | ||
} catch (e) { | ||
setLastSignature(undefined); | ||
setError(e); | ||
} finally { | ||
setIsSigningMessage(false); | ||
} | ||
}} | ||
> | ||
<Box flexGrow="1"> | ||
<TextField.Root | ||
placeholder="Write a message to sign" | ||
onChange={(e: SyntheticEvent<HTMLInputElement>) => setText(e.currentTarget.value)} | ||
value={text} | ||
> | ||
<TextField.Slot> | ||
<Pencil1Icon /> | ||
</TextField.Slot> | ||
</TextField.Root> | ||
</Box> | ||
<Dialog.Root | ||
open={!!lastSignature} | ||
onOpenChange={open => { | ||
if (!open) { | ||
setLastSignature(undefined); | ||
} | ||
}} | ||
> | ||
<Dialog.Trigger> | ||
<Button | ||
color={error ? undefined : 'red'} | ||
disabled={!text} | ||
loading={isSigningMessage} | ||
type="submit" | ||
> | ||
Sign Message | ||
</Button> | ||
</Dialog.Trigger> | ||
{lastSignature ? ( | ||
<Dialog.Content | ||
onClick={e => { | ||
e.stopPropagation(); | ||
}} | ||
> | ||
<Dialog.Title>You Signed a Message!</Dialog.Title> | ||
<DataList.Root orientation={{ initial: 'vertical', sm: 'horizontal' }}> | ||
<DataList.Item> | ||
<DataList.Label minWidth="88px">Message</DataList.Label> | ||
<DataList.Value> | ||
<Blockquote>{text}</Blockquote> | ||
</DataList.Value> | ||
</DataList.Item> | ||
<DataList.Item> | ||
<DataList.Label minWidth="88px">Signature</DataList.Label> | ||
<DataList.Value> | ||
<Code truncate>{getBase64Decoder().decode(lastSignature)}</Code> | ||
</DataList.Value> | ||
</DataList.Item> | ||
</DataList.Root> | ||
<Flex gap="3" mt="4" justify="end"> | ||
<Dialog.Close> | ||
<Button>Cool!</Button> | ||
</Dialog.Close> | ||
</Flex> | ||
</Dialog.Content> | ||
) : null} | ||
</Dialog.Root> | ||
{error !== NO_ERROR ? <ErrorDialog error={error} onClose={() => setError(NO_ERROR)} /> : null} | ||
</form> | ||
</Flex> | ||
); | ||
} |
101 changes: 101 additions & 0 deletions
101
examples/react-app/src/components/ConnectWalletMenu.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; | ||
import { Button, Callout, DropdownMenu } from '@radix-ui/themes'; | ||
import { StandardConnect, StandardDisconnect } from '@wallet-standard/core'; | ||
import type { UiWallet } from '@wallet-standard/react'; | ||
import { uiWalletAccountBelongsToUiWallet, useWallets } from '@wallet-standard/react'; | ||
import { useContext, useRef, useState, useTransition } from 'react'; | ||
import { ErrorBoundary } from 'react-error-boundary'; | ||
|
||
import { SelectedWalletAccountContext } from '../context/SelectedWalletAccountContext'; | ||
import { ConnectWalletMenuItem } from './ConnectWalletMenuItem'; | ||
import { ErrorDialog } from './ErrorDialog'; | ||
import { UnconnectableWalletMenuItem } from './UnconnectableWalletMenuItem'; | ||
import { WalletAccountIcon } from './WalletAccountIcon'; | ||
|
||
type Props = Readonly<{ | ||
children: React.ReactNode; | ||
}>; | ||
|
||
export function ConnectWalletMenu({ children }: Props) { | ||
const { current: NO_ERROR } = useRef(Symbol()); | ||
const wallets = useWallets(); | ||
const [selectedWalletAccount, setSelectedWalletAccount] = useContext(SelectedWalletAccountContext); | ||
const [error, setError] = useState<unknown | typeof NO_ERROR>(NO_ERROR); | ||
const [forceClose, setForceClose] = useState(false); | ||
const [_isPending, startTransition] = useTransition(); | ||
function renderItem(wallet: UiWallet) { | ||
return ( | ||
<ErrorBoundary | ||
fallbackRender={({ error }) => <UnconnectableWalletMenuItem error={error} wallet={wallet} />} | ||
key={`wallet:${wallet.name}`} | ||
> | ||
<ConnectWalletMenuItem | ||
onAccountSelect={account => { | ||
startTransition(() => { | ||
setSelectedWalletAccount(account); | ||
setForceClose(true); | ||
}); | ||
}} | ||
onDisconnect={wallet => { | ||
if (selectedWalletAccount && uiWalletAccountBelongsToUiWallet(selectedWalletAccount, wallet)) { | ||
startTransition(() => { | ||
setSelectedWalletAccount(undefined); | ||
}); | ||
} | ||
}} | ||
onError={setError} | ||
wallet={wallet} | ||
/> | ||
</ErrorBoundary> | ||
); | ||
} | ||
const walletsThatSupportStandardConnect = []; | ||
const unconnectableWallets = []; | ||
for (const wallet of wallets) { | ||
if (wallet.features.includes(StandardConnect) && wallet.features.includes(StandardDisconnect)) { | ||
walletsThatSupportStandardConnect.push(wallet); | ||
} else { | ||
unconnectableWallets.push(wallet); | ||
} | ||
} | ||
return ( | ||
<> | ||
<DropdownMenu.Root open={forceClose ? false : undefined} onOpenChange={setForceClose.bind(null, false)}> | ||
<DropdownMenu.Trigger> | ||
<Button> | ||
{selectedWalletAccount ? ( | ||
<> | ||
<WalletAccountIcon account={selectedWalletAccount} width="18" height="18" /> | ||
{selectedWalletAccount.address.slice(0, 8)} | ||
</> | ||
) : ( | ||
children | ||
)} | ||
<DropdownMenu.TriggerIcon /> | ||
</Button> | ||
</DropdownMenu.Trigger> | ||
<DropdownMenu.Content> | ||
{wallets.length === 0 ? ( | ||
<Callout.Root color="orange" highContrast> | ||
<Callout.Icon> | ||
<ExclamationTriangleIcon /> | ||
</Callout.Icon> | ||
<Callout.Text>This browser has no wallets installed.</Callout.Text> | ||
</Callout.Root> | ||
) : ( | ||
<> | ||
{walletsThatSupportStandardConnect.map(renderItem)} | ||
{unconnectableWallets.length ? ( | ||
<> | ||
<DropdownMenu.Separator /> | ||
{unconnectableWallets.map(renderItem)} | ||
</> | ||
) : null} | ||
</> | ||
)} | ||
</DropdownMenu.Content> | ||
</DropdownMenu.Root> | ||
{error !== NO_ERROR ? <ErrorDialog error={error} onClose={() => setError(NO_ERROR)} /> : null} | ||
</> | ||
); | ||
} |
91 changes: 91 additions & 0 deletions
91
examples/react-app/src/components/ConnectWalletMenuItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; | ||
import { DropdownMenu, ThickChevronRightIcon } from '@radix-ui/themes'; | ||
import type { UiWallet, UiWalletAccount } from '@wallet-standard/react'; | ||
import { useConnect, useDisconnect } from '@wallet-standard/react'; | ||
import { useCallback, useContext } from 'react'; | ||
|
||
import { SelectedWalletAccountContext } from '../context/SelectedWalletAccountContext'; | ||
import { WalletMenuItemContent } from './WalletMenuItemContent'; | ||
|
||
type Props = Readonly<{ | ||
onAccountSelect(account: UiWalletAccount | undefined): void; | ||
onDisconnect(wallet: UiWallet): void; | ||
onError(err: unknown): void; | ||
wallet: UiWallet; | ||
}>; | ||
|
||
export function ConnectWalletMenuItem({ onAccountSelect, onDisconnect, onError, wallet }: Props) { | ||
const [isConnecting, connect] = useConnect(wallet); | ||
const [isDisconnecting, disconnect] = useDisconnect(wallet); | ||
const isPending = isConnecting || isDisconnecting; | ||
const isConnected = wallet.accounts.length > 0; | ||
const [selectedWalletAccount] = useContext(SelectedWalletAccountContext); | ||
const handleClick = useCallback(async () => { | ||
try { | ||
if (isConnected) { | ||
await disconnect(); | ||
onDisconnect(wallet); | ||
} else { | ||
const accounts = await connect(); | ||
if (accounts[0]) { | ||
onAccountSelect(accounts[0]); | ||
} | ||
} | ||
} catch (e) { | ||
onError(e); | ||
} | ||
}, [connect, disconnect, isConnected, onAccountSelect, onDisconnect, onError, wallet]); | ||
return ( | ||
<DropdownMenu.Sub open={!isConnected ? false : undefined}> | ||
<DropdownMenuPrimitive.SubTrigger | ||
asChild={false} | ||
className={[ | ||
'rt-BaseMenuItem', | ||
'rt-BaseMenuSubTrigger', | ||
'rt-DropdownMenuItem', | ||
'rt-DropdownMenuSubTrigger', | ||
].join(' ')} | ||
disabled={isPending} | ||
onClick={!isConnected ? handleClick : undefined} | ||
> | ||
<WalletMenuItemContent loading={isPending} wallet={wallet} /> | ||
{isConnected ? ( | ||
<div className="rt-BaseMenuShortcut rt-DropdownMenuShortcut"> | ||
<ThickChevronRightIcon className="rt-BaseMenuSubTriggerIcon rt-DropdownMenuSubtriggerIcon" /> | ||
</div> | ||
) : null} | ||
</DropdownMenuPrimitive.SubTrigger> | ||
<DropdownMenu.SubContent> | ||
<DropdownMenu.Label>Accounts</DropdownMenu.Label> | ||
<DropdownMenu.RadioGroup value={selectedWalletAccount?.address}> | ||
{wallet.accounts.map(account => ( | ||
<DropdownMenu.RadioItem | ||
key={account.address} | ||
value={account.address} | ||
onSelect={() => { | ||
onAccountSelect(account); | ||
}} | ||
> | ||
{account.address.slice(0, 8)}… | ||
</DropdownMenu.RadioItem> | ||
))} | ||
</DropdownMenu.RadioGroup> | ||
<DropdownMenu.Separator /> | ||
<DropdownMenu.Item | ||
color="red" | ||
onSelect={async e => { | ||
e.preventDefault(); | ||
try { | ||
await disconnect(); | ||
onDisconnect(wallet); | ||
} catch (e) { | ||
onError(e); | ||
} | ||
}} | ||
> | ||
Disconnect | ||
</DropdownMenu.Item> | ||
</DropdownMenu.SubContent> | ||
</DropdownMenu.Sub> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { ExclamationTriangleIcon, ExitIcon } from '@radix-ui/react-icons'; | ||
import { Button, Tooltip } from '@radix-ui/themes'; | ||
import type { UiWallet } from '@wallet-standard/react'; | ||
import { useDisconnect } from '@wallet-standard/react'; | ||
import { useState } from 'react'; | ||
|
||
import { NO_ERROR } from '../errors'; | ||
|
||
type Props = Readonly<{ | ||
wallet: UiWallet; | ||
}>; | ||
|
||
export function DisconnectButton({ | ||
wallet, | ||
...buttonProps | ||
}: Omit<React.ComponentProps<typeof Button>, 'color' | 'loading' | 'onClick'> & Props) { | ||
const [isDisconnecting, disconnect] = useDisconnect(wallet); | ||
const [lastError, setLastError] = useState<unknown | typeof NO_ERROR>(NO_ERROR); | ||
return ( | ||
<Tooltip | ||
content={ | ||
<> | ||
Error:{' '} | ||
{lastError && typeof lastError === 'object' && 'message' in lastError | ||
? lastError.message | ||
: String(lastError)} | ||
</> | ||
} | ||
open={lastError !== NO_ERROR} | ||
side="left" | ||
> | ||
<Button | ||
{...buttonProps} | ||
color="red" | ||
loading={isDisconnecting} | ||
onClick={async () => { | ||
setLastError(NO_ERROR); | ||
try { | ||
await disconnect(); | ||
} catch (e) { | ||
setLastError(e); | ||
} | ||
}} | ||
variant="outline" | ||
> | ||
{lastError === NO_ERROR ? <ExitIcon /> : <ExclamationTriangleIcon />} | ||
Disconnect | ||
</Button> | ||
</Tooltip> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { AlertDialog, Blockquote, Button, Flex } from '@radix-ui/themes'; | ||
|
||
import { getErrorMessage } from '../errors'; | ||
|
||
type Props = Readonly<{ | ||
error: unknown; | ||
onClose(): void; | ||
}>; | ||
|
||
export function ErrorDialog({ error, onClose }: Props) { | ||
return ( | ||
<AlertDialog.Root | ||
open={true} | ||
onOpenChange={open => { | ||
if (!open) { | ||
onClose(); | ||
} | ||
}} | ||
> | ||
<AlertDialog.Content> | ||
<AlertDialog.Title color="red">We encountered the following error</AlertDialog.Title> | ||
<AlertDialog.Description> | ||
<Blockquote>{getErrorMessage(error, 'Unknown')}</Blockquote> | ||
</AlertDialog.Description> | ||
<Flex mt="4" justify="end"> | ||
<AlertDialog.Action> | ||
<Button variant="solid">Close</Button> | ||
</AlertDialog.Action> | ||
</Flex> | ||
</AlertDialog.Content> | ||
</AlertDialog.Root> | ||
); | ||
} |
23 changes: 23 additions & 0 deletions
23
examples/react-app/src/components/FeatureNotSupportedCallout.1.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; | ||
import { Callout } from '@radix-ui/themes'; | ||
import React from 'react'; | ||
import { FallbackProps } from 'react-error-boundary'; | ||
|
||
import { getErrorMessage } from '../errors'; | ||
|
||
interface Props extends Callout.RootProps, FallbackProps {} | ||
|
||
export function FeatureNotSupportedCallout({ | ||
error, | ||
resetErrorBoundary: _, | ||
...rootProps | ||
}: Props): React.ReactElement<typeof Callout.Root> { | ||
return ( | ||
<Callout.Root color="gray" size="1" {...rootProps} style={{ flexGrow: 1, ...rootProps.style }}> | ||
<Callout.Icon> | ||
<ExclamationTriangleIcon /> | ||
</Callout.Icon> | ||
<Callout.Text>{getErrorMessage(error, 'This account does not support this feature')}</Callout.Text> | ||
</Callout.Root> | ||
); | ||
} |
23 changes: 23 additions & 0 deletions
23
examples/react-app/src/components/FeatureNotSupportedCallout.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; | ||
import { Callout } from '@radix-ui/themes'; | ||
import React from 'react'; | ||
import type { FallbackProps } from 'react-error-boundary'; | ||
|
||
import { getErrorMessage } from '../errors'; | ||
|
||
interface Props extends Callout.RootProps, FallbackProps {} | ||
|
||
export function FeatureNotSupportedCallout({ | ||
error, | ||
resetErrorBoundary: _, | ||
...rootProps | ||
}: Props): React.ReactElement<typeof Callout.Root> { | ||
return ( | ||
<Callout.Root color="gray" size="1" {...rootProps} style={{ flexGrow: 1, ...rootProps.style }}> | ||
<Callout.Icon> | ||
<ExclamationTriangleIcon /> | ||
</Callout.Icon> | ||
<Callout.Text>{getErrorMessage(error, 'This account does not support this feature')}</Callout.Text> | ||
</Callout.Root> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { DataList } from '@radix-ui/themes'; | ||
import React from 'react'; | ||
|
||
type Props = Readonly<{ | ||
children: React.ReactNode; | ||
label: React.ReactNode; | ||
}>; | ||
|
||
export function FeaturePanel({ children, label }: Props) { | ||
return ( | ||
<DataList.Item align={{ initial: 'start', sm: 'center' }}> | ||
<DataList.Label>{label}</DataList.Label> | ||
<DataList.Value style={{ width: '100%' }}>{children}</DataList.Value> | ||
</DataList.Item> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { Badge, Box, DropdownMenu, Flex, Heading, Spinner } from '@radix-ui/themes'; | ||
import { useContext, useTransition } from 'react'; | ||
|
||
import { ChainContext } from '../context/ChainContext'; | ||
import { ConnectWalletMenu } from './ConnectWalletMenu'; | ||
|
||
export function Nav() { | ||
const { displayName: currentChainName, chain, setChain } = useContext(ChainContext); | ||
const [isPending, startTransition] = useTransition(); | ||
const currentChainBadge = ( | ||
<Badge color="gray" style={{ verticalAlign: 'middle' }}> | ||
{currentChainName} <Spinner loading={isPending} /> | ||
</Badge> | ||
); | ||
return ( | ||
<Box | ||
style={{ | ||
backgroundColor: 'var(--gray-1)', | ||
borderBottom: '1px solid var(--gray-a6)', | ||
zIndex: 1, | ||
}} | ||
position="sticky" | ||
p="3" | ||
top="0" | ||
> | ||
<Flex gap="4" justify="between" align="center"> | ||
<Heading as="h1" size={{ initial: '4', xs: '6' }} truncate> | ||
Solana React App{' '} | ||
{setChain ? ( | ||
<DropdownMenu.Root> | ||
<DropdownMenu.Trigger>{currentChainBadge}</DropdownMenu.Trigger> | ||
<DropdownMenu.Content> | ||
<DropdownMenu.RadioGroup | ||
onValueChange={value => { | ||
startTransition(() => { | ||
setChain(value as 'solana:${string}'); | ||
}); | ||
}} | ||
value={chain} | ||
> | ||
<DropdownMenu.RadioItem value="solana:mainnet">Mainnet Beta</DropdownMenu.RadioItem> | ||
<DropdownMenu.RadioItem value="solana:devnet">Devnet</DropdownMenu.RadioItem> | ||
<DropdownMenu.RadioItem value="solana:testnet">Testnet</DropdownMenu.RadioItem> | ||
</DropdownMenu.RadioGroup> | ||
</DropdownMenu.Content> | ||
</DropdownMenu.Root> | ||
) : ( | ||
currentChainBadge | ||
)} | ||
</Heading> | ||
<ConnectWalletMenu>Connect Wallet</ConnectWalletMenu> | ||
</Flex> | ||
</Box> | ||
); | ||
} |
210 changes: 210 additions & 0 deletions
210
examples/react-app/src/components/SolanaSignAndSendTransactionFeaturePanel.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
import { Blockquote, Box, Button, Dialog, Flex, Link, Select, Text, TextField } from '@radix-ui/themes'; | ||
import { useWalletAccountTransactionSendingSigner } from '@solana/react'; | ||
import { | ||
address, | ||
appendTransactionMessageInstruction, | ||
createTransactionMessage, | ||
getBase58Decoder, | ||
lamports, | ||
pipe, | ||
setTransactionMessageFeePayerSigner, | ||
setTransactionMessageLifetimeUsingBlockhash, | ||
signAndSendTransactionMessageWithSigners, | ||
} from '@solana/web3.js'; | ||
import { getTransferSolInstruction } from '@solana-program/system'; | ||
import { getUiWalletAccountStorageKey, type UiWalletAccount, useWallets } from '@wallet-standard/react'; | ||
import type { SyntheticEvent } from 'react'; | ||
import { useContext, useId, useMemo, useRef, useState } from 'react'; | ||
import { useSWRConfig } from 'swr'; | ||
|
||
import { ChainContext } from '../context/ChainContext'; | ||
import { RpcContext } from '../context/RpcContext'; | ||
import { ErrorDialog } from './ErrorDialog'; | ||
import { WalletMenuItemContent } from './WalletMenuItemContent'; | ||
|
||
type Props = Readonly<{ | ||
account: UiWalletAccount; | ||
}>; | ||
|
||
function solStringToLamports(solQuantityString: string) { | ||
if (Number.isNaN(parseFloat(solQuantityString))) { | ||
throw new Error('Could not parse token quantity: ' + String(solQuantityString)); | ||
} | ||
const numDecimals = BigInt(solQuantityString.split('.')[1]?.length ?? 0); | ||
const bigIntLamports = BigInt(solQuantityString.replace('.', '')) * 10n ** (9n - numDecimals); | ||
return lamports(bigIntLamports); | ||
} | ||
|
||
export function SolanaSignAndSendTransactionFeaturePanel({ account }: Props) { | ||
const { mutate } = useSWRConfig(); | ||
const { current: NO_ERROR } = useRef(Symbol()); | ||
const { rpc } = useContext(RpcContext); | ||
const wallets = useWallets(); | ||
const [isSendingTransaction, setIsSendingTransaction] = useState(false); | ||
const [error, setError] = useState<unknown | typeof NO_ERROR>(NO_ERROR); | ||
const [lastSignature, setLastSignature] = useState<Uint8Array | undefined>(); | ||
const [solQuantityString, setSolQuantityString] = useState<string>(''); | ||
const [recipientAccountStorageKey, setRecipientAccountStorageKey] = useState<string | undefined>(); | ||
const recipientAccount = useMemo(() => { | ||
if (recipientAccountStorageKey) { | ||
for (const wallet of wallets) { | ||
for (const account of wallet.accounts) { | ||
if (getUiWalletAccountStorageKey(account) === recipientAccountStorageKey) { | ||
return account; | ||
} | ||
} | ||
} | ||
} | ||
}, [recipientAccountStorageKey, wallets]); | ||
const { chain: currentChain, solanaExplorerClusterName } = useContext(ChainContext); | ||
const transactionSendingSigner = useWalletAccountTransactionSendingSigner(account, currentChain); | ||
const lamportsInputId = useId(); | ||
const recipientSelectId = useId(); | ||
return ( | ||
<Flex asChild gap="2" direction={{ initial: 'column', sm: 'row' }} style={{ width: '100%' }}> | ||
<form | ||
onSubmit={async e => { | ||
e.preventDefault(); | ||
setError(NO_ERROR); | ||
setIsSendingTransaction(true); | ||
try { | ||
const amount = solStringToLamports(solQuantityString); | ||
if (!recipientAccount) { | ||
throw new Error('The address of the recipient could not be found'); | ||
} | ||
const { value: latestBlockhash } = await rpc | ||
.getLatestBlockhash({ commitment: 'confirmed' }) | ||
.send(); | ||
const message = pipe( | ||
createTransactionMessage({ version: 0 }), | ||
m => setTransactionMessageFeePayerSigner(transactionSendingSigner, m), | ||
m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), | ||
m => | ||
appendTransactionMessageInstruction( | ||
getTransferSolInstruction({ | ||
amount, | ||
destination: address(recipientAccount.address), | ||
source: transactionSendingSigner, | ||
}), | ||
m, | ||
), | ||
); | ||
const signature = await signAndSendTransactionMessageWithSigners( | ||
// @ts-expect-error FIXME(#2818) | ||
message, | ||
); | ||
mutate({ address: transactionSendingSigner.address, chain: currentChain }); | ||
mutate({ address: recipientAccount.address, chain: currentChain }); | ||
setLastSignature(signature); | ||
setSolQuantityString(''); | ||
} catch (e) { | ||
setLastSignature(undefined); | ||
setError(e); | ||
} finally { | ||
setIsSendingTransaction(false); | ||
} | ||
}} | ||
> | ||
<Box flexGrow="1" overflow="hidden"> | ||
<Flex gap="3" align="center"> | ||
<Box flexGrow="1" minWidth="90px" maxWidth="130px"> | ||
<TextField.Root | ||
disabled={isSendingTransaction} | ||
id={lamportsInputId} | ||
placeholder="Amount" | ||
onChange={(e: SyntheticEvent<HTMLInputElement>) => | ||
setSolQuantityString(e.currentTarget.value) | ||
} | ||
style={{ width: 'auto' }} | ||
type="number" | ||
value={solQuantityString} | ||
> | ||
<TextField.Slot side="right">{'\u25ce'}</TextField.Slot> | ||
</TextField.Root> | ||
</Box> | ||
<Box flexShrink="0"> | ||
<Text as="label" color="gray" htmlFor={recipientSelectId} weight="medium"> | ||
To Account | ||
</Text> | ||
</Box> | ||
<Select.Root | ||
disabled={isSendingTransaction} | ||
onValueChange={setRecipientAccountStorageKey} | ||
value={recipientAccount ? getUiWalletAccountStorageKey(recipientAccount) : undefined} | ||
> | ||
<Select.Trigger | ||
style={{ flexGrow: 1, flexShrink: 1, overflow: 'hidden' }} | ||
placeholder="Select a Connected Account" | ||
/> | ||
<Select.Content> | ||
{wallets.flatMap(wallet => | ||
wallet.accounts | ||
.filter(({ chains }) => chains.includes(currentChain)) | ||
.map(account => { | ||
const key = getUiWalletAccountStorageKey(account); | ||
return ( | ||
<Select.Item key={key} value={key}> | ||
<WalletMenuItemContent wallet={wallet}> | ||
{account.address} | ||
</WalletMenuItemContent> | ||
</Select.Item> | ||
); | ||
}), | ||
)} | ||
</Select.Content> | ||
</Select.Root> | ||
</Flex> | ||
</Box> | ||
<Dialog.Root | ||
open={!!lastSignature} | ||
onOpenChange={open => { | ||
if (!open) { | ||
setLastSignature(undefined); | ||
} | ||
}} | ||
> | ||
<Dialog.Trigger> | ||
<Button | ||
color={error ? undefined : 'red'} | ||
disabled={solQuantityString === '' || !recipientAccount} | ||
loading={isSendingTransaction} | ||
type="submit" | ||
> | ||
Transfer | ||
</Button> | ||
</Dialog.Trigger> | ||
{lastSignature ? ( | ||
<Dialog.Content | ||
onClick={e => { | ||
e.stopPropagation(); | ||
}} | ||
> | ||
<Dialog.Title>You transferred tokens!</Dialog.Title> | ||
<Flex direction="column" gap="2"> | ||
<Text>Signature:</Text> | ||
<Blockquote>{getBase58Decoder().decode(lastSignature)}</Blockquote> | ||
<Text> | ||
<Link | ||
href={`https://explorer.solana.com/tx/${getBase58Decoder().decode( | ||
lastSignature, | ||
)}?cluster=${solanaExplorerClusterName}`} | ||
target="_blank" | ||
> | ||
View this transaction | ||
</Link>{' '} | ||
on Explorer | ||
</Text> | ||
</Flex> | ||
<Flex gap="3" mt="4" justify="end"> | ||
<Dialog.Close> | ||
<Button>Cool!</Button> | ||
</Dialog.Close> | ||
</Flex> | ||
</Dialog.Content> | ||
) : null} | ||
</Dialog.Root> | ||
{error !== NO_ERROR ? <ErrorDialog error={error} onClose={() => setError(NO_ERROR)} /> : null} | ||
</form> | ||
</Flex> | ||
); | ||
} |
32 changes: 32 additions & 0 deletions
32
examples/react-app/src/components/SolanaSignMessageFeaturePanel.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { useWalletAccountMessageSigner } from '@solana/react'; | ||
import type { Address } from '@solana/web3.js'; | ||
import type { ReadonlyUint8Array } from '@wallet-standard/core'; | ||
import type { UiWalletAccount } from '@wallet-standard/react'; | ||
import { useCallback } from 'react'; | ||
|
||
import { BaseSignMessageFeaturePanel } from './BaseSignMessageFeaturePanel'; | ||
|
||
type Props = Readonly<{ | ||
account: UiWalletAccount; | ||
}>; | ||
|
||
export function SolanaSignMessageFeaturePanel({ account }: Props) { | ||
const messageSigner = useWalletAccountMessageSigner(account); | ||
const signMessage = useCallback( | ||
async (message: ReadonlyUint8Array) => { | ||
const [result] = await messageSigner.modifyAndSignMessages([ | ||
{ | ||
content: message as Uint8Array, | ||
signatures: {}, | ||
}, | ||
]); | ||
const signature = result?.signatures[account.address as Address]; | ||
if (!signature) { | ||
throw new Error(); | ||
} | ||
return signature as ReadonlyUint8Array; | ||
}, | ||
[account.address, messageSigner], | ||
); | ||
return <BaseSignMessageFeaturePanel signMessage={signMessage} />; | ||
} |
32 changes: 32 additions & 0 deletions
32
examples/react-app/src/components/UnconnectableWalletMenuItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; | ||
import { Box, DropdownMenu, Text } from '@radix-ui/themes'; | ||
import type { UiWallet } from '@wallet-standard/react'; | ||
import { useState } from 'react'; | ||
|
||
import { ErrorDialog } from './ErrorDialog'; | ||
import { WalletMenuItemContent } from './WalletMenuItemContent'; | ||
|
||
type Props = Readonly<{ | ||
error: unknown; | ||
wallet: UiWallet; | ||
}>; | ||
|
||
export function UnconnectableWalletMenuItem({ error, wallet }: Props) { | ||
const [dialogIsOpen, setDialogIsOpen] = useState(false); | ||
return ( | ||
<> | ||
<DropdownMenu.Item disabled onClick={() => setDialogIsOpen(true)}> | ||
<WalletMenuItemContent wallet={wallet}> | ||
<Text style={{ textDecoration: 'line-through' }}>{wallet.name}</Text> | ||
</WalletMenuItemContent> | ||
<Box className="rt-BaseMenuShortcut rt-DropdownMenuShortcut"> | ||
<ExclamationTriangleIcon | ||
className="rt-BaseMenuSubTriggerIcon rt-DropdownMenuSubtriggerIcon" | ||
style={{ height: 14, width: 14 }} | ||
/> | ||
</Box> | ||
</DropdownMenu.Item> | ||
{dialogIsOpen ? <ErrorDialog error={error} onClose={() => setDialogIsOpen(false)} /> : null} | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import type { UiWalletAccount } from '@wallet-standard/react'; | ||
import { uiWalletAccountBelongsToUiWallet,useWallets } from '@wallet-standard/react'; | ||
import React from 'react'; | ||
|
||
type Props = React.ComponentProps<'img'> & | ||
Readonly<{ | ||
account: UiWalletAccount; | ||
}>; | ||
|
||
export function WalletAccountIcon({ account, ...imgProps }: Props) { | ||
const wallets = useWallets(); | ||
let icon; | ||
if (account.icon) { | ||
icon = account.icon; | ||
} else { | ||
for (const wallet of wallets) { | ||
if (uiWalletAccountBelongsToUiWallet(account, wallet)) { | ||
icon = wallet.icon; | ||
break; | ||
} | ||
} | ||
} | ||
return icon ? <img src={icon} {...imgProps} /> : null; | ||
} |
25 changes: 25 additions & 0 deletions
25
examples/react-app/src/components/WalletMenuItemContent.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { Avatar, Flex, Spinner, Text } from '@radix-ui/themes'; | ||
import type { UiWallet } from '@wallet-standard/react'; | ||
import React from 'react'; | ||
|
||
type Props = Readonly<{ | ||
children?: React.ReactNode; | ||
loading?: boolean; | ||
wallet: UiWallet; | ||
}>; | ||
|
||
export function WalletMenuItemContent({ children, loading, wallet }: Props) { | ||
return ( | ||
<Flex align="center" gap="2"> | ||
<Spinner loading={!!loading}> | ||
<Avatar | ||
fallback={<Text size="1">{wallet.name.slice(0, 1)}</Text>} | ||
radius="none" | ||
src={wallet.icon} | ||
style={{ height: 18, width: 18 }} | ||
/> | ||
</Spinner> | ||
<Text truncate>{children ?? wallet.name}</Text> | ||
</Flex> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import type { ClusterUrl } from '@solana/web3.js'; | ||
import { devnet, mainnet, testnet } from '@solana/web3.js'; | ||
import React, { createContext, useMemo, useState } from 'react'; | ||
|
||
import { localStorage } from '../storage'; | ||
|
||
const STORAGE_KEY = 'solana-wallet-standard-example-react:selected-chain'; | ||
|
||
type Context = Readonly<{ | ||
chain: `solana:${string}`; | ||
displayName: string; | ||
setChain?(chain: 'solana:${string}'): void; | ||
solanaExplorerClusterName: 'devnet' | 'mainnet-beta' | 'testnet'; | ||
solanaRpcSubscriptionsUrl: ClusterUrl; | ||
solanaRpcUrl: ClusterUrl; | ||
}>; | ||
|
||
export const ChainContext = createContext<Context>({ | ||
chain: 'solana:devnet', | ||
displayName: 'Devnet', | ||
solanaExplorerClusterName: 'devnet', | ||
solanaRpcSubscriptionsUrl: devnet('wss://api.devnet.solana.com'), | ||
solanaRpcUrl: devnet('https://api.devnet.solana.com'), | ||
}); | ||
|
||
export function ChainContextProvider({ children }: { children: React.ReactNode }) { | ||
const [chain, setChain] = useState(() => localStorage.getItem(STORAGE_KEY) ?? 'solana:devnet'); | ||
const contextValue = useMemo<Context>(() => { | ||
switch (chain) { | ||
case 'solana:mainnet': | ||
return { | ||
chain: 'solana:mainnet', | ||
displayName: 'Mainnet Beta', | ||
solanaExplorerClusterName: 'mainnet-beta', | ||
solanaRpcSubscriptionsUrl: mainnet('wss://api.mainnet-beta.solana.com'), | ||
solanaRpcUrl: mainnet('https://api.mainnet-beta.solana.com'), | ||
}; | ||
case 'solana:testnet': | ||
return { | ||
chain: 'solana:testnet', | ||
displayName: 'Testnet', | ||
solanaExplorerClusterName: 'testnet', | ||
solanaRpcSubscriptionsUrl: testnet('wss://api.testnet.solana.com'), | ||
solanaRpcUrl: testnet('https://api.testnet.solana.com'), | ||
}; | ||
case 'solana:devnet': | ||
default: | ||
if (chain !== 'solana:devnet') { | ||
localStorage.removeItem(STORAGE_KEY); | ||
console.error(`Unrecognized chain \`${chain}\``); | ||
} | ||
return { | ||
chain: 'solana:devnet', | ||
displayName: 'Devnet', | ||
solanaExplorerClusterName: 'devnet', | ||
solanaRpcSubscriptionsUrl: devnet('wss://api.devnet.solana.com'), | ||
solanaRpcUrl: devnet('https://api.devnet.solana.com'), | ||
}; | ||
} | ||
}, [chain]); | ||
return ( | ||
<ChainContext.Provider | ||
value={useMemo( | ||
() => ({ | ||
...contextValue, | ||
setChain(chain) { | ||
localStorage.setItem(STORAGE_KEY, chain); | ||
setChain(chain); | ||
}, | ||
}), | ||
[contextValue], | ||
)} | ||
> | ||
{children} | ||
</ChainContext.Provider> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import type { Rpc, RpcSubscriptions, SolanaRpcApiMainnet, SolanaRpcSubscriptionsApi } from '@solana/web3.js'; | ||
import { createSolanaRpc, createSolanaRpcSubscriptions, devnet } from '@solana/web3.js'; | ||
import type { ReactNode } from 'react'; | ||
import { createContext, useContext, useMemo } from 'react'; | ||
|
||
import { ChainContext } from './ChainContext'; | ||
|
||
export const RpcContext = createContext<{ | ||
rpc: Rpc<SolanaRpcApiMainnet>; // Limit the API to only those methods found on Mainnet (ie. not `requestAirdrop`) | ||
rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>; | ||
}>({ | ||
rpc: createSolanaRpc(devnet('https://api.devnet.solana.com')), | ||
rpcSubscriptions: createSolanaRpcSubscriptions(devnet('wss://api.devnet.solana.com')), | ||
}); | ||
|
||
type Props = Readonly<{ | ||
children: ReactNode; | ||
}>; | ||
|
||
export function RpcContextProvider({ children }: Props) { | ||
const { solanaRpcSubscriptionsUrl, solanaRpcUrl } = useContext(ChainContext); | ||
return ( | ||
<RpcContext.Provider | ||
value={useMemo( | ||
() => ({ | ||
rpc: createSolanaRpc(solanaRpcUrl), | ||
rpcSubscriptions: createSolanaRpcSubscriptions(solanaRpcSubscriptionsUrl), | ||
}), | ||
[solanaRpcSubscriptionsUrl, solanaRpcUrl], | ||
)} | ||
> | ||
{children} | ||
</RpcContext.Provider> | ||
); | ||
} |
113 changes: 113 additions & 0 deletions
113
examples/react-app/src/context/SelectedWalletAccountContext.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import type { UiWallet, UiWalletAccount } from '@wallet-standard/react'; | ||
import { | ||
getUiWalletAccountStorageKey, | ||
uiWalletAccountBelongsToUiWallet, | ||
uiWalletAccountsAreSame, | ||
useWallets, | ||
} from '@wallet-standard/react'; | ||
import { createContext, useEffect, useMemo, useState } from 'react'; | ||
|
||
import { localStorage } from '../storage'; | ||
|
||
type State = UiWalletAccount | undefined; | ||
|
||
const STORAGE_KEY = 'solana-wallet-standard-example-react:selected-wallet-and-address'; | ||
|
||
export const SelectedWalletAccountContext = createContext< | ||
readonly [selectedWalletAccount: State, setSelectedWalletAccount: React.Dispatch<React.SetStateAction<State>>] | ||
>([ | ||
undefined, | ||
() => { | ||
/* empty */ | ||
}, | ||
]); | ||
|
||
let wasSetInvoked = false; | ||
function getSavedWalletAccount(wallets: readonly UiWallet[]): UiWalletAccount | undefined { | ||
if (wasSetInvoked) { | ||
// After the user makes an explicit choice of wallet, stop trying to auto-select the | ||
// saved wallet, if and when it appears. | ||
return; | ||
} | ||
try { | ||
const savedWalletNameAndAddress = localStorage.getItem(STORAGE_KEY); | ||
if (!savedWalletNameAndAddress || typeof savedWalletNameAndAddress !== 'string') { | ||
return; | ||
} | ||
const [savedWalletName, savedAccountAddress] = savedWalletNameAndAddress.split(':'); | ||
if (!savedWalletName || !savedAccountAddress) { | ||
return; | ||
} | ||
for (const wallet of wallets) { | ||
if (wallet.name === savedWalletName) { | ||
for (const account of wallet.accounts) { | ||
if (account.address === savedAccountAddress) { | ||
return account; | ||
} | ||
} | ||
} | ||
} | ||
} catch { | ||
return; | ||
} | ||
} | ||
|
||
export function SelectedWalletAccountContextProvider({ children }: { children: React.ReactNode }) { | ||
const wallets = useWallets(); | ||
const [selectedWalletAccount, setSelectedWalletAccountInternal] = useState<State>(() => | ||
getSavedWalletAccount(wallets), | ||
); | ||
const setSelectedWalletAccount: React.Dispatch<React.SetStateAction<State>> = s => { | ||
setSelectedWalletAccountInternal(prevSelectedWalletAccount => { | ||
wasSetInvoked = true; | ||
const nextWalletAccount = typeof s === 'function' ? s(prevSelectedWalletAccount) : s; | ||
const accountKey = nextWalletAccount ? getUiWalletAccountStorageKey(nextWalletAccount) : undefined; | ||
try { | ||
if (accountKey) { | ||
localStorage.setItem(STORAGE_KEY, accountKey); | ||
} else { | ||
localStorage.removeItem(STORAGE_KEY); | ||
} | ||
} catch { | ||
/* empty */ | ||
} | ||
return nextWalletAccount; | ||
}); | ||
}; | ||
useEffect(() => { | ||
const savedWalletAccount = getSavedWalletAccount(wallets); | ||
if (savedWalletAccount) { | ||
setSelectedWalletAccountInternal(savedWalletAccount); | ||
} | ||
}, [wallets]); | ||
const walletAccount = useMemo(() => { | ||
if (selectedWalletAccount) { | ||
for (const uiWallet of wallets) { | ||
for (const uiWalletAccount of uiWallet.accounts) { | ||
if (uiWalletAccountsAreSame(selectedWalletAccount, uiWalletAccount)) { | ||
return uiWalletAccount; | ||
} | ||
} | ||
if (uiWalletAccountBelongsToUiWallet(selectedWalletAccount, uiWallet) && uiWallet.accounts[0]) { | ||
// If the selected account belongs to this connected wallet, at least, then | ||
// select one of its accounts. | ||
return uiWallet.accounts[0]; | ||
} | ||
} | ||
} | ||
}, [selectedWalletAccount, wallets]); | ||
useEffect(() => { | ||
// If there is a selected wallet account but the wallet to which it belongs has since | ||
// disconnected, clear the selected wallet. | ||
if (selectedWalletAccount && !walletAccount) { | ||
setSelectedWalletAccountInternal(undefined); | ||
} | ||
}, [selectedWalletAccount, walletAccount]); | ||
return ( | ||
<SelectedWalletAccountContext.Provider | ||
value={useMemo(() => [walletAccount, setSelectedWalletAccount] as const, [walletAccount])} | ||
> | ||
{children} | ||
</SelectedWalletAccountContext.Provider> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { Code, Flex, Text } from '@radix-ui/themes'; | ||
import { | ||
isWalletStandardError, | ||
WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_CHAIN_UNSUPPORTED, | ||
WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED, | ||
WALLET_STANDARD_ERROR__FEATURES__WALLET_FEATURE_UNIMPLEMENTED, | ||
} from '@wallet-standard/core'; | ||
import React from 'react'; | ||
|
||
export const NO_ERROR = Symbol(); | ||
|
||
export function getErrorMessage(err: unknown, fallbackMessage: React.ReactNode): React.ReactNode { | ||
if (isWalletStandardError(err, WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED)) { | ||
return ( | ||
<> | ||
This account does not support the <Code>{err.context.featureName}</Code> feature | ||
</> | ||
); | ||
} else if (isWalletStandardError(err, WALLET_STANDARD_ERROR__FEATURES__WALLET_FEATURE_UNIMPLEMENTED)) { | ||
return ( | ||
<Flex direction="column" gap="4"> | ||
<Text as="p"> | ||
The wallet '{err.context.walletName}' ( | ||
{err.context.supportedChains.sort().map((chain, ii, { length }) => ( | ||
<React.Fragment key={chain}> | ||
<Code>{chain}</Code> | ||
{ii === length - 1 ? null : ', '} | ||
</React.Fragment> | ||
))} | ||
) does not support the <Code>{err.context.featureName}</Code> feature. | ||
</Text> | ||
<Text as="p" trim="end"> | ||
Features supported: | ||
<ul> | ||
{err.context.supportedFeatures.sort().map(featureName => ( | ||
<li key={featureName}> | ||
<Code>{featureName}</Code> | ||
</li> | ||
))} | ||
</ul> | ||
</Text> | ||
</Flex> | ||
); | ||
} else if (isWalletStandardError(err, WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_CHAIN_UNSUPPORTED)) { | ||
return ( | ||
<Flex direction="column" gap="4"> | ||
<Text as="p"> | ||
This account does not support the chain <Code>{err.context.chain}</Code>. | ||
</Text> | ||
<Text as="p" trim="end"> | ||
Chains supported: | ||
<ul> | ||
{err.context.supportedChains.sort().map(chain => ( | ||
<li key={chain}> | ||
<Code>{chain}</Code> | ||
</li> | ||
))} | ||
</ul> | ||
</Text> | ||
</Flex> | ||
); | ||
} else if (err && typeof err === 'object' && 'message' in err) { | ||
return String(err.message); | ||
} | ||
return fallbackMessage; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
@import './reset.css'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import './index.css'; | ||
import '@radix-ui/themes/styles.css'; | ||
|
||
import { Flex, Section, Theme } from '@radix-ui/themes'; | ||
import { StrictMode } from 'react'; | ||
import { createRoot } from 'react-dom/client'; | ||
|
||
import { Nav } from './components/Nav.tsx'; | ||
import { ChainContextProvider } from './context/ChainContext.tsx'; | ||
import { RpcContextProvider } from './context/RpcContext.tsx'; | ||
import { SelectedWalletAccountContextProvider } from './context/SelectedWalletAccountContext.tsx'; | ||
import Root from './routes/root.tsx'; | ||
|
||
const rootNode = document.getElementById('root')!; | ||
const root = createRoot(rootNode); | ||
root.render( | ||
<StrictMode> | ||
<Theme> | ||
<Flex direction="column"> | ||
<ChainContextProvider> | ||
<SelectedWalletAccountContextProvider> | ||
<RpcContextProvider> | ||
<Nav /> | ||
<Section> | ||
<Root /> | ||
</Section> | ||
</RpcContextProvider> | ||
</SelectedWalletAccountContextProvider> | ||
</ChainContextProvider> | ||
</Flex> | ||
</Theme> | ||
</StrictMode>, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/* Box sizing rules */ | ||
*, | ||
*::before, | ||
*::after { | ||
box-sizing: border-box; | ||
} | ||
|
||
/* Remove default margin */ | ||
body, | ||
h1, | ||
h2, | ||
h3, | ||
h4, | ||
p, | ||
figure, | ||
blockquote, | ||
dl, | ||
dd { | ||
margin: 0; | ||
} | ||
|
||
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ | ||
ul[role='list'], | ||
ol[role='list'] { | ||
list-style: none; | ||
} | ||
|
||
/* Set core root defaults */ | ||
html:focus-within { | ||
scroll-behavior: smooth; | ||
} | ||
|
||
/* Set core body defaults */ | ||
body { | ||
min-height: 100vh; | ||
text-rendering: optimizeSpeed; | ||
line-height: 1.5; | ||
} | ||
|
||
/* A elements that don't have a class get default styles */ | ||
a:not([class]) { | ||
text-decoration-skip-ink: auto; | ||
} | ||
|
||
/* Make images easier to work with */ | ||
img, | ||
picture { | ||
max-width: 100%; | ||
display: block; | ||
} | ||
|
||
/* Inherit fonts for inputs and buttons */ | ||
input, | ||
button, | ||
textarea, | ||
select { | ||
font: inherit; | ||
} | ||
|
||
/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */ | ||
@media (prefers-reduced-motion: reduce) { | ||
html:focus-within { | ||
scroll-behavior: auto; | ||
} | ||
|
||
*, | ||
*::before, | ||
*::after { | ||
animation-duration: 0.01ms !important; | ||
animation-iteration-count: 1 !important; | ||
transition-duration: 0.01ms !important; | ||
scroll-behavior: auto !important; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { Box, Code, Container, DataList, Flex, Heading, Spinner, Text } from '@radix-ui/themes'; | ||
import { Suspense, useContext } from 'react'; | ||
import { ErrorBoundary } from 'react-error-boundary'; | ||
|
||
import { Balance } from '../components/Balance'; | ||
import { FeatureNotSupportedCallout } from '../components/FeatureNotSupportedCallout'; | ||
import { FeaturePanel } from '../components/FeaturePanel'; | ||
import { SolanaSignAndSendTransactionFeaturePanel } from '../components/SolanaSignAndSendTransactionFeaturePanel'; | ||
import { SolanaSignMessageFeaturePanel } from '../components/SolanaSignMessageFeaturePanel'; | ||
import { WalletAccountIcon } from '../components/WalletAccountIcon'; | ||
import { ChainContext } from '../context/ChainContext'; | ||
import { SelectedWalletAccountContext } from '../context/SelectedWalletAccountContext'; | ||
|
||
function Root() { | ||
const { chain } = useContext(ChainContext); | ||
const [selectedWalletAccount] = useContext(SelectedWalletAccountContext); | ||
return ( | ||
<Container mx={{ initial: '3', xs: '6' }}> | ||
{selectedWalletAccount ? ( | ||
<Flex gap="6" direction="column"> | ||
<Flex gap="2"> | ||
<Flex align="center" gap="3" flexGrow="1"> | ||
<WalletAccountIcon account={selectedWalletAccount} height="48" width="48" /> | ||
<Box> | ||
<Heading as="h4" size="3"> | ||
{selectedWalletAccount.label ?? 'Unlabeled Account'} | ||
</Heading> | ||
<Code variant="outline" truncate size={{ initial: '1', xs: '2' }}> | ||
{selectedWalletAccount.address} | ||
</Code> | ||
</Box> | ||
</Flex> | ||
<Flex direction="column" align="start"> | ||
<Heading as="h4" size="3"> | ||
Balance | ||
</Heading> | ||
<ErrorBoundary | ||
fallback={<Text>–</Text>} | ||
key={`${selectedWalletAccount.address}:${chain}`} | ||
> | ||
<Suspense | ||
fallback={ | ||
<Spinner loading> | ||
<Balance account={selectedWalletAccount} /> | ||
</Spinner> | ||
} | ||
> | ||
<Balance account={selectedWalletAccount} /> | ||
</Suspense> | ||
</ErrorBoundary> | ||
</Flex> | ||
</Flex> | ||
<DataList.Root orientation={{ initial: 'vertical', sm: 'horizontal' }} size="3"> | ||
<FeaturePanel label="Sign Message"> | ||
<ErrorBoundary FallbackComponent={FeatureNotSupportedCallout}> | ||
<SolanaSignMessageFeaturePanel account={selectedWalletAccount} /> | ||
</ErrorBoundary> | ||
</FeaturePanel> | ||
<FeaturePanel label="Sign And Send Transaction"> | ||
<ErrorBoundary FallbackComponent={FeatureNotSupportedCallout}> | ||
<SolanaSignAndSendTransactionFeaturePanel account={selectedWalletAccount} /> | ||
</ErrorBoundary> | ||
</FeaturePanel> | ||
</DataList.Root> | ||
</Flex> | ||
) : ( | ||
<Text as="p">Click “Connect Wallet” to get started.</Text> | ||
)} | ||
</Container> | ||
); | ||
} | ||
|
||
export default Root; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
let storage: Storage | undefined; | ||
try { | ||
if (typeof window !== 'undefined' && window.localStorage) { | ||
storage = window.localStorage; | ||
} | ||
} catch { | ||
/* empty */ | ||
} | ||
|
||
function guard<TArgs extends unknown[], TReturn, TFallbackReturn>( | ||
fn: (...args: TArgs) => TReturn, | ||
fallbackReturn: TFallbackReturn, | ||
): (...args: TArgs) => TFallbackReturn | TReturn; | ||
function guard<TArgs extends unknown[], TReturn>( | ||
fn: (...args: TArgs) => TReturn, | ||
): (...args: TArgs) => TReturn | undefined; | ||
function guard<TArgs extends unknown[], TReturn, TFallbackReturn>( | ||
fn: (...args: TArgs) => TReturn, | ||
fallbackReturn?: TFallbackReturn, | ||
): (...args: TArgs) => TFallbackReturn | TReturn | undefined { | ||
return (...args) => { | ||
try { | ||
return fn(...args); | ||
} catch (e) { | ||
console.error(e); | ||
return fallbackReturn; | ||
} | ||
}; | ||
} | ||
|
||
export const localStorage: Pick<Storage, 'getItem' | 'removeItem' | 'setItem'> = { | ||
getItem: guard(k => { | ||
return storage?.getItem(k) ?? null; | ||
}, null), | ||
removeItem: guard(k => { | ||
storage?.removeItem(k); | ||
}), | ||
setItem: guard((k, v) => { | ||
storage?.setItem(k, v); | ||
}), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/// <reference types="vite/client" /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"compilerOptions": { | ||
"target": "ES2020", | ||
"useDefineForClassFields": true, | ||
"lib": ["ES2020", "DOM", "DOM.Iterable"], | ||
"module": "ESNext", | ||
"skipLibCheck": true, | ||
|
||
/* Bundler mode */ | ||
"moduleResolution": "bundler", | ||
"allowImportingTsExtensions": true, | ||
"resolveJsonModule": true, | ||
"isolatedModules": true, | ||
"noEmit": true, | ||
"jsx": "react-jsx", | ||
|
||
/* Linting */ | ||
"strict": true, | ||
"noUnusedLocals": true, | ||
"noUnusedParameters": true, | ||
"noFallthroughCasesInSwitch": true | ||
}, | ||
"include": ["vite.config.ts", "src"], | ||
"references": [{ "path": "./tsconfig.node.json" }] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"compilerOptions": { | ||
"composite": true, | ||
"skipLibCheck": true, | ||
"module": "ESNext", | ||
"moduleResolution": "bundler", | ||
"allowSyntheticDefaultImports": true, | ||
"strict": true | ||
}, | ||
"include": ["vite.config.ts"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import react from '@vitejs/plugin-react-swc'; | ||
import { defineConfig, Plugin } from 'vite'; | ||
|
||
function replaceProcessEnv(mode: string): Plugin { | ||
const nodeEnvRegex = /process(\.env(\.NODE_ENV)|\["env"\]\.NODE_ENV)/g; | ||
return { | ||
name: 'replace-process-env', | ||
renderChunk(code) { | ||
return code.replace(nodeEnvRegex, JSON.stringify(mode)); | ||
}, | ||
}; | ||
} | ||
|
||
// https://vitejs.dev/config/ | ||
export default defineConfig(({ mode }) => ({ | ||
define: { | ||
'process.env': process.env, | ||
}, | ||
plugins: [react(), replaceProcessEnv(mode)], | ||
})); |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
packages: | ||
- "examples/*" | ||
- "packages/*" |