This repository has been archived by the owner on Jan 22, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 925
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make React context in the example app work with hot module replacement
- Loading branch information
1 parent
99b7ff3
commit 658eadb
Showing
7 changed files
with
203 additions
and
191 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 |
---|---|---|
@@ -1,77 +1,22 @@ | ||
import type { ClusterUrl } from '@solana/web3.js'; | ||
import { devnet, mainnet, testnet } from '@solana/web3.js'; | ||
import React, { createContext, useMemo, useState } from 'react'; | ||
import { devnet } from '@solana/web3.js'; | ||
import { createContext } from 'react'; | ||
|
||
import { localStorage } from '../storage'; | ||
|
||
const STORAGE_KEY = 'solana-example-react-app:selected-chain'; | ||
|
||
type Context = Readonly<{ | ||
export type ChainContext = Readonly<{ | ||
chain: `solana:${string}`; | ||
displayName: string; | ||
setChain?(chain: 'solana:${string}'): void; | ||
setChain?(chain: `solana:${string}`): void; | ||
solanaExplorerClusterName: 'devnet' | 'mainnet-beta' | 'testnet'; | ||
solanaRpcSubscriptionsUrl: ClusterUrl; | ||
solanaRpcUrl: ClusterUrl; | ||
}>; | ||
|
||
const DEFAULT_CHAIN_CONFIG = Object.freeze({ | ||
export const DEFAULT_CHAIN_CONFIG = Object.freeze({ | ||
chain: 'solana:devnet', | ||
displayName: 'Devnet', | ||
solanaExplorerClusterName: 'devnet', | ||
solanaRpcSubscriptionsUrl: devnet('wss://api.devnet.solana.com'), | ||
solanaRpcUrl: devnet('https://api.devnet.solana.com'), | ||
}); | ||
|
||
export const ChainContext = createContext<Context>(DEFAULT_CHAIN_CONFIG); | ||
|
||
export function ChainContextProvider({ children }: { children: React.ReactNode }) { | ||
const [chain, setChain] = useState(() => localStorage.getItem(STORAGE_KEY) ?? 'solana:devnet'); | ||
const contextValue = useMemo<Context>(() => { | ||
switch (chain) { | ||
// @ts-expect-error Intentional fall through | ||
case 'solana:mainnet': | ||
if (process.env.REACT_EXAMPLE_APP_ENABLE_MAINNET === 'true') { | ||
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'), | ||
}; | ||
} | ||
// falls through | ||
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 DEFAULT_CHAIN_CONFIG; | ||
} | ||
}, [chain]); | ||
return ( | ||
<ChainContext.Provider | ||
value={useMemo( | ||
() => ({ | ||
...contextValue, | ||
setChain(chain) { | ||
localStorage.setItem(STORAGE_KEY, chain); | ||
setChain(chain); | ||
}, | ||
}), | ||
[contextValue], | ||
)} | ||
> | ||
{children} | ||
</ChainContext.Provider> | ||
); | ||
} | ||
export const ChainContext = createContext<ChainContext>(DEFAULT_CHAIN_CONFIG); |
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,57 @@ | ||
import { mainnet, testnet } from '@solana/web3.js'; | ||
import { useMemo, useState } from 'react'; | ||
|
||
import { ChainContext, DEFAULT_CHAIN_CONFIG } from './ChainContext'; | ||
|
||
const STORAGE_KEY = 'solana-example-react-app:selected-chain'; | ||
|
||
export function ChainContextProvider({ children }: { children: React.ReactNode }) { | ||
const [chain, setChain] = useState(() => localStorage.getItem(STORAGE_KEY) ?? 'solana:devnet'); | ||
const contextValue = useMemo<ChainContext>(() => { | ||
switch (chain) { | ||
// @ts-expect-error Intentional fall through | ||
case 'solana:mainnet': | ||
if (process.env.REACT_EXAMPLE_APP_ENABLE_MAINNET === 'true') { | ||
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'), | ||
}; | ||
} | ||
// falls through | ||
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 DEFAULT_CHAIN_CONFIG; | ||
} | ||
}, [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
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,26 @@ | ||
import { createSolanaRpc, createSolanaRpcSubscriptions } from '@solana/web3.js'; | ||
import { ReactNode, useContext, useMemo } from 'react'; | ||
|
||
import { ChainContext } from './ChainContext'; | ||
import { RpcContext } from './RpcContext'; | ||
|
||
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> | ||
); | ||
} |
109 changes: 7 additions & 102 deletions
109
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 |
---|---|---|
@@ -1,111 +1,16 @@ | ||
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 type { UiWalletAccount } from '@wallet-standard/react'; | ||
import { createContext } from 'react'; | ||
|
||
import { localStorage } from '../storage'; | ||
|
||
type State = UiWalletAccount | undefined; | ||
|
||
const STORAGE_KEY = 'solana-wallet-standard-example-react:selected-wallet-and-address'; | ||
export type SelectedWalletAccountState = UiWalletAccount | undefined; | ||
|
||
export const SelectedWalletAccountContext = createContext< | ||
readonly [selectedWalletAccount: State, setSelectedWalletAccount: React.Dispatch<React.SetStateAction<State>>] | ||
readonly [ | ||
selectedWalletAccount: SelectedWalletAccountState, | ||
setSelectedWalletAccount: React.Dispatch<React.SetStateAction<SelectedWalletAccountState>>, | ||
] | ||
>([ | ||
undefined /* selectedWalletAccount */, | ||
function setSelectedWalletAccount() { | ||
/* empty */ | ||
}, | ||
]); | ||
|
||
let wasSetterInvoked = false; | ||
function getSavedWalletAccount(wallets: readonly UiWallet[]): UiWalletAccount | undefined { | ||
if (wasSetterInvoked) { | ||
// After the user makes an explicit choice of wallet, stop trying to auto-select the | ||
// saved wallet, if and when it appears. | ||
return; | ||
} | ||
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; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Saves the selected wallet account's storage key to the browser's local storage. In future | ||
* sessions it will try to return that same wallet account, or at least one from the same brand of | ||
* wallet if the wallet from which it came is still in the Wallet Standard registry. | ||
*/ | ||
export function SelectedWalletAccountContextProvider({ children }: { children: React.ReactNode }) { | ||
const wallets = useWallets(); | ||
const [selectedWalletAccount, setSelectedWalletAccountInternal] = useState<State>(() => | ||
getSavedWalletAccount(wallets), | ||
); | ||
const setSelectedWalletAccount: React.Dispatch<React.SetStateAction<State>> = setStateAction => { | ||
setSelectedWalletAccountInternal(prevSelectedWalletAccount => { | ||
wasSetterInvoked = true; | ||
const nextWalletAccount = | ||
typeof setStateAction === 'function' ? setStateAction(prevSelectedWalletAccount) : setStateAction; | ||
const accountKey = nextWalletAccount ? getUiWalletAccountStorageKey(nextWalletAccount) : undefined; | ||
if (accountKey) { | ||
localStorage.setItem(STORAGE_KEY, accountKey); | ||
} else { | ||
localStorage.removeItem(STORAGE_KEY); | ||
} | ||
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], [walletAccount])} | ||
> | ||
{children} | ||
</SelectedWalletAccountContext.Provider> | ||
); | ||
} |
Oops, something went wrong.