Skip to content

Commit

Permalink
feat: you can choose between all available pubkeys in the React Nativ…
Browse files Browse the repository at this point in the history
…e app
  • Loading branch information
steveluscher committed Jul 28, 2022
1 parent 8169e18 commit 84c1fa7
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 49 deletions.
19 changes: 13 additions & 6 deletions examples/example-react-native-app/components/AccountBalance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,21 @@ type Props = Readonly<{
export default function AccountBalance({publicKey}: Props) {
const {connection} = useConnection();
const balanceFetcher = useCallback(
async function (_: 'accountBalance'): Promise<number> {
return await connection.getBalance(publicKey);
async function ([_, selectedPublicKey]: [
'accountBalance',
PublicKey,
]): Promise<number> {
return await connection.getBalance(selectedPublicKey);
},
[connection],
);
const {data: lamports} = useSWR(
['accountBalance', publicKey],
balanceFetcher,
{
suspense: true,
},
[connection, publicKey],
);
const {data: lamports} = useSWR('accountBalance', balanceFetcher, {
suspense: true,
});
const balance = useMemo(
() =>
new Intl.NumberFormat(undefined, {maximumFractionDigits: 1}).format(
Expand Down
79 changes: 67 additions & 12 deletions examples/example-react-native-app/components/AccountInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
import {PublicKey} from '@solana/web3.js';
import React, {Suspense, useMemo} from 'react';
import React, {Suspense, useMemo, useState} from 'react';
import {ActivityIndicator, Linking, StyleSheet, View} from 'react-native';
import {Card, Subheading, Surface, useTheme} from 'react-native-paper';
import {
Button,
Card,
Menu,
Subheading,
Surface,
useTheme,
} from 'react-native-paper';

import {Account} from '../utils/useAuthorization';
import AccountBalance from './AccountBalance';
import DisconnectButton from './DisconnectButton';
import FundAccountButton from './FundAccountButton';

type Props = Readonly<{
publicKey: PublicKey;
accounts: Account[];
onChange(nextSelectedAccount: Account): void;
selectedAccount: Account;
}>;

export default function AccountInfo({publicKey}: Props) {
export default function AccountInfo({
accounts,
onChange,
selectedAccount,
}: Props) {
const {colors} = useTheme();
const publicKeyBase58String = useMemo(
() => publicKey.toBase58(),
[publicKey],
const selectedAccountPublicKeyBase58String = useMemo(
() => selectedAccount.publicKey.toBase58(),
[selectedAccount],
);
const [menuVisible, setMenuVisible] = useState(false);
return (
<Surface elevation={4} style={styles.container}>
<Card.Content>
<Suspense fallback={<ActivityIndicator />}>
<View style={styles.balanceRow}>
<AccountBalance publicKey={publicKey} />
<FundAccountButton publicKey={publicKey}>
<AccountBalance publicKey={selectedAccount.publicKey} />
<FundAccountButton publicKey={selectedAccount.publicKey}>
Add Funds
</FundAccountButton>
</View>
Expand All @@ -32,12 +46,44 @@ export default function AccountInfo({publicKey}: Props) {
numberOfLines={1}
onPress={() => {
Linking.openURL(
`https://explorer.solana.com/address/${publicKeyBase58String}?cluster=devnet`,
`https://explorer.solana.com/address/${selectedAccountPublicKeyBase58String}?cluster=devnet`,
);
}}
style={styles.keyRow}>
{'\u{1f5dd} ' + publicKeyBase58String}
{'\u{1f5dd} ' + selectedAccountPublicKeyBase58String}
</Subheading>
{accounts.length > 1 ? (
<Menu
anchor={
<Button
onPress={() => setMenuVisible(true)}
style={styles.addressMenuTrigger}>
Change Address
</Button>
}
onDismiss={() => {
setMenuVisible(false);
}}
style={styles.addressMenu}
visible={menuVisible}>
{accounts.map(account => {
const base58PublicKey = account.publicKey.toBase58();
return (
<Menu.Item
disabled={account.address === selectedAccount.address}
style={styles.addressMenuItem}
contentStyle={styles.addressMenuItem}
onPress={() => {
onChange(account);
setMenuVisible(false);
}}
key={base58PublicKey}
title={base58PublicKey}
/>
);
})}
</Menu>
) : null}
<DisconnectButton buttonColor={colors.error} mode="contained">
Disconnect
</DisconnectButton>
Expand All @@ -47,6 +93,15 @@ export default function AccountInfo({publicKey}: Props) {
}

const styles = StyleSheet.create({
addressMenu: {
end: 18,
},
addressMenuItem: {
maxWidth: '100%',
},
addressMenuTrigger: {
marginBottom: 12,
},
balanceRow: {
flexDirection: 'row',
justifyContent: 'space-between',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default function FundAccountButton({children, publicKey}: Props) {
} else {
setSnackbarProps({children: 'Funding successful'});
mutate(
'accountBalance',
['accountBalance', publicKey],
// Optimistic update; will be revalidated automatically by SWR.
(currentBalance?: number) =>
(currentBalance || 0) + LAMPORTS_PER_AIRDROP,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type Props = Readonly<{
}>;

export default function RecordMessageButton({children, message}: Props) {
const {account: selectedAccount, authorizeSession} = useAuthorization();
const {authorizeSession, selectedAccount} = useAuthorization();
const {connection} = useConnection();
const setSnackbarProps = useContext(SnackbarContext);
const [recordMessageTutorialOpen, setRecordMessageTutorialOpen] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type Props = Readonly<{
}>;

export default function SignMessageButton({children, message}: Props) {
const {account: selectedAccount, authorizeSession} = useAuthorization();
const {authorizeSession, selectedAccount} = useAuthorization();
const [previewSignature, setPreviewSignature] = useState<Uint8Array | null>(
null,
);
Expand Down
10 changes: 8 additions & 2 deletions examples/example-react-native-app/screens/MainScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import SignMessageButton from '../components/SignMessageButton';
import useAuthorization from '../utils/useAuthorization';

export default function MainScreen() {
const {account} = useAuthorization();
const {accounts, onChangeAccount, selectedAccount} = useAuthorization();
const [memoText, setMemoText] = useState('');
return (
<>
Expand Down Expand Up @@ -36,7 +36,13 @@ export default function MainScreen() {
<Divider style={styles.spacer} />
<SignMessageButton message={memoText}>Sign Message</SignMessageButton>
</ScrollView>
{account ? <AccountInfo publicKey={account.publicKey} /> : null}
{accounts && selectedAccount ? (
<AccountInfo
accounts={accounts}
onChange={onChangeAccount}
selectedAccount={selectedAccount}
/>
) : null}
</Portal.Host>
</>
);
Expand Down
110 changes: 84 additions & 26 deletions examples/example-react-native-app/utils/useAuthorization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,51 @@ import {
ReauthorizeAPI,
} from '@solana-mobile/mobile-wallet-adapter-protocol';
import {toUint8Array} from 'js-base64';
import {useCallback} from 'react';
import {useCallback, useMemo} from 'react';
import useSWR from 'swr';

type Account = Readonly<{
export type Account = Readonly<{
address: Base64EncodedAddress;
authToken: AuthToken;
publicKey: PublicKey;
}>;

const STORAGE_KEY = 'cachedAuthorization';
type Authorization = Readonly<{
accounts: Account[];
authToken: AuthToken;
selectedAccount: Account;
}>;

function getAccountFromAuthorizationResult(
authorizationResult: AuthorizationResult,
): Account {
const address = authorizationResult.addresses[0]; // TODO(#44): support multiple addresses
function getAccountFromAddress(address: Base64EncodedAddress): Account {
return {
address,
authToken: authorizationResult.auth_token,
publicKey: getPublicKeyFromAddress(address),
};
}

function getAuthorizationFromAuthorizationResult(
authorizationResult: AuthorizationResult,
previouslySelectedAccount?: Account,
): Authorization {
let selectedAccount: Account;
if (
// We have yet to select an account.
previouslySelectedAccount == null ||
// The previously selected account is no longer in the set of authorized addresses.
authorizationResult.addresses.indexOf(previouslySelectedAccount.address) ===
-1
) {
const firstAddress = authorizationResult.addresses[0];
selectedAccount = getAccountFromAddress(firstAddress);
} else {
selectedAccount = previouslySelectedAccount;
}
return {
accounts: authorizationResult.addresses.map(getAccountFromAddress),
authToken: authorizationResult.auth_token,
selectedAccount,
};
}

function getPublicKeyFromAddress(address: Base64EncodedAddress): PublicKey {
const publicKeyByteArray = toUint8Array(address);
return new PublicKey(publicKeyByteArray);
Expand All @@ -40,38 +63,73 @@ export const APP_IDENTITY = {
};

export default function useAuthorization() {
const {data: account, mutate} = useSWR<Account | null | undefined>(
STORAGE_KEY,
const {data: authorization, mutate: setAuthorization} = useSWR<
Authorization | null | undefined
>('authorization');
const handleAuthorizationResult = useCallback(
async (
authorizationResult: AuthorizationResult,
): Promise<Authorization> => {
const nextAuthorization = getAuthorizationFromAuthorizationResult(
authorizationResult,
authorization?.selectedAccount,
);
await setAuthorization(nextAuthorization);
return nextAuthorization;
},
[authorization, setAuthorization],
);
const authorizeSession = useCallback(
async (wallet: AuthorizeAPI & ReauthorizeAPI) => {
const authorizationResult = await (account
const authorizationResult = await (authorization
? wallet.reauthorize({
auth_token: account.authToken,
auth_token: authorization.authToken,
})
: wallet.authorize({
cluster: 'devnet',
identity: APP_IDENTITY,
}));
return await mutate(
getAccountFromAuthorizationResult(authorizationResult),
);
return (await handleAuthorizationResult(authorizationResult))
.selectedAccount;
},
[account, mutate],
[authorization],
);
const deauthorizeSession = useCallback(
async (wallet: DeauthorizeAPI) => {
if (account?.authToken == null) {
if (authorization?.authToken == null) {
return;
}
await wallet.deauthorize({auth_token: account?.authToken});
mutate(null);
await wallet.deauthorize({auth_token: authorization.authToken});
setAuthorization(null);
},
[account, mutate],
[authorization, setAuthorization],
);
const onChangeAccount = useCallback((nextSelectedAccount: Account) => {
setAuthorization(currentAuthorization => {
if (
!currentAuthorization?.accounts.some(
({address}) => address === nextSelectedAccount.address,
)
) {
throw new Error(
`${nextSelectedAccount.address} is not one of the available addresses`,
);
}

return {
...currentAuthorization,
selectedAccount: nextSelectedAccount,
};
});
}, []);
return useMemo(
() => ({
accounts: authorization?.accounts ?? null,
authorizeSession,
deauthorizeSession,
onChangeAccount,
selectedAccount: authorization?.selectedAccount ?? null,
}),
[authorization, authorizeSession, deauthorizeSession, onChangeAccount],
);
return {
account: account ?? null,
authorizeSession,
deauthorizeSession,
};
}

0 comments on commit 84c1fa7

Please sign in to comment.