Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Commit

Permalink
A useSignIn() hook for accessing the Sign In With Solana feature
Browse files Browse the repository at this point in the history
  • Loading branch information
steveluscher committed Jul 10, 2024
1 parent a3cb9fa commit 46ad8e6
Show file tree
Hide file tree
Showing 9 changed files with 547 additions and 28 deletions.
34 changes: 34 additions & 0 deletions .changeset/fuzzy-lizards-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
'@solana/react': minor
---

Added a `useSignIn` hook that, given a `UiWallet` or `UiWalletAccount`, returns a function that you can call to trigger a wallet's [‘Sign In With Solana’](https://phantom.app/learn/developers/sign-in-with-solana) feature.

#### Example

```tsx
import { useSignIn } from '@solana/react';

function SignInButton({ wallet }) {
const csrfToken = useCsrfToken();
const signIn = useSignIn(wallet);
return (
<button
onClick={async () => {
try {
const { account, signedMessage, signature } = await signIn({
domain: window.location.host,
nonce: csrfToken,
});
// Inspect the contents of `signedMessage` and verify it against `signature`
window.alert(`You are now signed in with the address ${account.address}`);
} catch (e) {
console.error('Failed to sign in', e);
}
}}
>
Sign In
</button>
);
}
```
63 changes: 35 additions & 28 deletions examples/react-app/src/components/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { useContext, useTransition } from 'react';

import { ChainContext } from '../context/ChainContext';
import { ConnectWalletMenu } from './ConnectWalletMenu';
import { SignInMenu } from './SignInMenu';
import { useWallets } from '@wallet-standard/react';
import { SolanaSignIn } from '@solana/wallet-standard-features';

export function Nav() {
const { displayName: currentChainName, chain, setChain } = useContext(ChainContext);
const [isPending, startTransition] = useTransition();
const wallets = useWallets();
const currentChainBadge = (
<Badge color="gray" style={{ verticalAlign: 'middle' }}>
{currentChainName} <Spinner loading={isPending} />
Expand All @@ -24,35 +28,38 @@ export function Nav() {
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}
>
{process.env.REACT_EXAMPLE_APP_ENABLE_MAINNET === 'true' ? (
<DropdownMenu.RadioItem value="solana:mainnet">
Mainnet Beta
</DropdownMenu.RadioItem>
) : null}
<DropdownMenu.RadioItem value="solana:devnet">Devnet</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="solana:testnet">Testnet</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
) : (
currentChainBadge
)}
</Heading>
<Box flexGrow="1">
<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}
>
{process.env.REACT_EXAMPLE_APP_ENABLE_MAINNET === 'true' ? (
<DropdownMenu.RadioItem value="solana:mainnet">
Mainnet Beta
</DropdownMenu.RadioItem>
) : null}
<DropdownMenu.RadioItem value="solana:devnet">Devnet</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="solana:testnet">Testnet</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
) : (
currentChainBadge
)}
</Heading>
</Box>
<ConnectWalletMenu>Connect Wallet</ConnectWalletMenu>
<SignInMenu>Sign In</SignInMenu>
</Flex>
</Box>
);
Expand Down
81 changes: 81 additions & 0 deletions examples/react-app/src/components/SignInMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
import { Button, Callout, DropdownMenu } from '@radix-ui/themes';
import { SolanaSignIn } from '@solana/wallet-standard-features';
import type { UiWallet } from '@wallet-standard/react';
import { useWallets } from '@wallet-standard/react';
import { useContext, useRef, useState, useTransition } from 'react';

import { SelectedWalletAccountContext } from '../context/SelectedWalletAccountContext';
import { ErrorDialog } from './ErrorDialog';
import { SignInMenuItem } from './SignInMenuItem';
import { ErrorBoundary } from 'react-error-boundary';
import { UnconnectableWalletMenuItem } from './UnconnectableWalletMenuItem';

type Props = Readonly<{
children: React.ReactNode;
}>;

export function SignInMenu({ children }: Props) {
const { current: NO_ERROR } = useRef(Symbol());
const wallets = useWallets();
const [_, 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}`}
>
<SignInMenuItem
onSignIn={account => {
startTransition(() => {
setSelectedWalletAccount(account);
setForceClose(true);
});
}}
onError={setError}
wallet={wallet}
/>
</ErrorBoundary>
);
}
const walletsThatSupportSignInWithSolana = [];
for (const wallet of wallets) {
if (wallet.features.includes(SolanaSignIn)) {
walletsThatSupportSignInWithSolana.push(wallet);
}
}
return (
<>
<DropdownMenu.Root open={forceClose ? false : undefined} onOpenChange={setForceClose.bind(null, false)}>
<DropdownMenu.Trigger>
<Button>
{children}
<DropdownMenu.TriggerIcon />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{walletsThatSupportSignInWithSolana.length === 0 ? (
<Callout.Root color="orange" highContrast>
<Callout.Icon>
<ExclamationTriangleIcon />
</Callout.Icon>
<Callout.Text>
This browser has no wallets installed that support{' '}
<a href="https://phantom.app/learn/developers/sign-in-with-solana" target="_blank">
Sign In With Solana
</a>
.
</Callout.Text>
</Callout.Root>
) : (
walletsThatSupportSignInWithSolana.map(renderItem)
)}
</DropdownMenu.Content>
</DropdownMenu.Root>
{error !== NO_ERROR ? <ErrorDialog error={error} onClose={() => setError(NO_ERROR)} /> : null}
</>
);
}
42 changes: 42 additions & 0 deletions examples/react-app/src/components/SignInMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { DropdownMenu } from '@radix-ui/themes';
import { useSignIn } from '@solana/react';
import type { UiWallet, UiWalletAccount } from '@wallet-standard/react';
import React, { useCallback, useState } from 'react';

import { WalletMenuItemContent } from './WalletMenuItemContent';

type Props = Readonly<{
onError(err: unknown): void;
onSignIn(account: UiWalletAccount | undefined): void;
wallet: UiWallet;
}>;

export function SignInMenuItem({ onSignIn, onError, wallet }: Props) {
const signIn = useSignIn(wallet);
const [isSigningIn, setIsSigningIn] = useState(false);
const handleSignInClick = useCallback(
async (e: React.MouseEvent) => {
e.preventDefault();
try {
setIsSigningIn(true);
try {
const { account } = await signIn({
domain: window.location.host,
statement: 'You will enjoy being signed in.',
});
onSignIn(account);
} finally {
setIsSigningIn(false);
}
} catch (e) {
onError(e);
}
},
[signIn, onSignIn, onError],
);
return (
<DropdownMenu.Item onClick={handleSignInClick}>
<WalletMenuItemContent loading={isSigningIn} wallet={wallet} />
</DropdownMenu.Item>
);
}
33 changes: 33 additions & 0 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,39 @@ This package contains React hooks for building Solana apps.

## Hooks

### `useSignIn(uiWalletAccount, chain)`

Given a `UiWallet` or `UiWalletAccount` this hook returns a function that you can call to trigger a wallet's [&lsquo;Sign In With Solana&rsquo;](https://phantom.app/learn/developers/sign-in-with-solana) feature.

#### Example

```tsx
import { useSignIn } from '@solana/react';

function SignInButton({ wallet }) {
const csrfToken = useCsrfToken();
const signIn = useSignIn(wallet);
return (
<button
onClick={async () => {
try {
const { account, signedMessage, signature } = await signIn({
domain: window.location.host,
nonce: csrfToken,
});
// Inspect the contents of `signedMessage` and verify it against `signature`
window.alert(`You are now signed in with the address ${account.address}`);
} catch (e) {
console.error('Failed to sign in', e);
}
}}
>
Sign In
</button>
);
}
```

### `useWalletAccountMessageSigner(uiWalletAccount)`

Given a `UiWalletAccount`, this hook returns an object that conforms to the `MessageModifyingSigner` interface of `@solana/signers`.
Expand Down
Loading

0 comments on commit 46ad8e6

Please sign in to comment.