
Authorization Provider
Todo dApp React Native profissional precisa de um padrão de gerenciamento de estado para conexões de carteira. Espalhar chamadas transact() pelos componentes leva a manipulação duplicada de auth tokens, estados de erro inconsistentes e componentes que não sabem sobre as ações de carteira uns dos outros.
O AuthorizationProvider resolve isso. É um React Context que possui todo o estado da carteira (contas conectadas, auth tokens e funções de autorização) e os disponibiliza para qualquer componente no seu app.
O Problema
Sem estado centralizado, você acaba com código como este espalhado por todo lado:
// Componente A
const cachedToken = await AsyncStorage.getItem('auth_token');
await transact(async (wallet) => {
await wallet.authorize({ identity, auth_token: cachedToken });
// fazer algo
});
// Componente B (mesmo código, duplicado)
const cachedToken = await AsyncStorage.getItem('auth_token');
await transact(async (wallet) => {
await wallet.authorize({ identity, auth_token: cachedToken });
// fazer outra coisa
});Problemas:
Cada componente gerencia seu próprio cache de token
Sem conhecimento compartilhado de qual conta está "selecionada"
Lógica de autorização duplicada
Difícil implementar "desconectar" que afete todo o app
Arquitetura do Provider
O AuthorizationProvider possui três coisas:
Estado de Autorização: contas, auth token, conta selecionada
Funções de Sessão:
authorizeSession,deauthorizeSessionSeletor de Conta: para carteiras com múltiplas contas
Componentes usam o hook useAuthorization para acessar este estado e estas funções.
// Qualquer componente pode fazer isso:
function SendButton() {
const { selectedAccount, authorizeSession } = useAuthorization();
const handleSend = async () => {
await transact(async (wallet) => {
await authorizeSession(wallet); // Provider cuida dos tokens
await wallet.signAndSendTransactions({ /* ... */ });
});
};
if (!selectedAccount) return null;
return <Button onPress={handleSend}>Enviar</Button>;
}Definições de Tipo
Primeiro, defina os formatos dos nossos dados:
// src/providers/types.ts
import { PublicKey } from '@solana/web3.js';
import {
AuthorizeAPI,
DeauthorizeAPI,
Base64EncodedAddress,
} from '@solana-mobile/mobile-wallet-adapter-protocol';
export interface Account {
address: Base64EncodedAddress; // base64, como retornado pelo MWA
label?: string;
publicKey: PublicKey; // Convertido para conveniência
}
export interface Authorization {
accounts: Account[];
authToken: string;
selectedAccount: Account;
}
export interface AuthorizationContextValue {
// Estado
accounts: Account[] | null;
selectedAccount: Account | null;
// Ações
authorizeSession: (wallet: AuthorizeAPI) => Promise<Account>;
deauthorizeSession: (wallet: DeauthorizeAPI) => Promise<void>;
onChangeAccount: (account: Account) => void;
}Implementação do Provider
Aqui está o AuthorizationProvider completo:
// src/providers/AuthorizationProvider.tsx
import React, {
createContext,
useContext,
useState,
useCallback,
useMemo,
ReactNode,
useEffect,
} from 'react';
import { PublicKey } from '@solana/web3.js';
import {
AuthorizeAPI,
DeauthorizeAPI,
AuthorizationResult,
Account as MWAAccount,
} from '@solana-mobile/mobile-wallet-adapter-protocol';
import { toByteArray } from 'react-native-quick-base64';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Account, Authorization, AuthorizationContextValue } from './types';
const APP_IDENTITY = {
name: 'Meu dApp Solana',
uri: 'https://mydapp.com',
icon: 'favicon.ico',
};
const CLUSTER = 'solana:devnet';
const AUTH_TOKEN_KEY='***';
// Converter formato de conta MWA para nosso tipo Account
function convertAccount(mwaAccount: MWAAccount): Account {
return {
address: mwaAccount.address,
label: mwaAccount.label,
publicKey: new PublicKey(toByteArray(mwaAccount.address)),
};
}
// Criar o contexto
const AuthorizationContext = createContext<AuthorizationContextValue>({
accounts: null,
selectedAccount: null,
authorizeSession: async () => {
throw new Error('AuthorizationProvider não montado');
},
deauthorizeSession: async () => {
throw new Error('AuthorizationProvider não montado');
},
onChangeAccount: () => {
throw new Error('AuthorizationProvider não montado');
},
});
// O componente provider
export function AuthorizationProvider({ children }: { children: ReactNode }) {
const [authorization, setAuthorization] = useState<Authorization | null>(null);
// Carregar auth token em cache na montagem
useEffect(() => {
AsyncStorage.getItem(AUTH_TOKEN_KEY).then((token) => {
if (token) {
// Temos um token mas sem info de conta ainda
// A próxima chamada authorizeSession populará as contas
console.log('Auth token em cache encontrado');
}
});
}, []);
// Lidar com resultado de autorização da carteira
const handleAuthorizationResult = useCallback(
async (result: AuthorizationResult): Promise<Authorization> => {
const accounts = result.accounts.map(convertAccount);
// Determinar qual conta selecionar
let selectedAccount: Account;
if (
authorization?.selectedAccount &&
accounts.some((a) => a.address === authorization.selectedAccount.address)
) {
// Manter a conta previamente selecionada se ainda disponível
selectedAccount = authorization.selectedAccount;
} else {
// Selecionar a primeira conta
selectedAccount = accounts[0];
}
const newAuth: Authorization = {
accounts,
authToken: result.auth_token,
selectedAccount,
};
// Cachear o token
await AsyncStorage.setItem(AUTH_TOKEN_KEY, result.auth_token);
setAuthorization(newAuth);
return newAuth;
},
[authorization?.selectedAccount]
);
// Autorizar uma sessão (chamado dentro do callback transact)
const authorizeSession = useCallback(
async (wallet: AuthorizeAPI): Promise<Account> => {
const cachedToken = await AsyncStorage.getItem(AUTH_TOKEN_KEY);
const result = await wallet.authorize({
identity: APP_IDENTITY,
chain: CLUSTER,
auth_token: cachedToken ?? undefined,
});
const auth = await handleAuthorizationResult(result);
return auth.selectedAccount;
},
[handleAuthorizationResult]
);
// Desautorizar (chamado dentro do callback transact)
const deauthorizeSession = useCallback(
async (wallet: DeauthorizeAPI): Promise<void> => {
const authToken = authorization?.authToken;
if (!authToken) return;
await wallet.deauthorize({ auth_token: authToken });
await AsyncStorage.removeItem(AUTH_TOKEN_KEY);
setAuthorization(null);
},
[authorization?.authToken]
);
// Trocar conta selecionada
const onChangeAccount = useCallback(
(account: Account): void => {
if (!authorization) return;
const exists = authorization.accounts.some(
(a) => a.address === account.address
);
if (!exists) {
throw new Error('Conta não está no conjunto autorizado');
}
setAuthorization((prev) =>
prev ? { ...prev, selectedAccount: account } : null
);
},
[authorization]
);
const value = useMemo(
(): AuthorizationContextValue => ({
accounts: authorization?.accounts ?? null,
selectedAccount: authorization?.selectedAccount ?? null,
authorizeSession,
deauthorizeSession,
onChangeAccount,
}),
[authorization, authorizeSession, deauthorizeSession, onChangeAccount]
);
return (
<AuthorizationContext.Provider value={value}>
{children}
</AuthorizationContext.Provider>
);
}
// Hook para componentes consumidores
export function useAuthorization(): AuthorizationContextValue {
return useContext(AuthorizationContext);
}Usando o Provider
Envolva seu app com o provider:
// App.tsx
import { AuthorizationProvider } from './providers/AuthorizationProvider';
import { ConnectionProvider } from './providers/ConnectionProvider';
import { MainScreen } from './screens/MainScreen';
export default function App() {
return (
<ConnectionProvider>
<AuthorizationProvider>
<MainScreen />
</AuthorizationProvider>
</ConnectionProvider>
);
}Agora qualquer componente pode usar a autorização:
// screens/MainScreen.tsx
import { transact } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import { useAuthorization } from '../providers/AuthorizationProvider';
export function MainScreen() {
const { selectedAccount, authorizeSession } = useAuthorization();
const handleConnect = async () => {
await transact(async (wallet) => {
const account = await authorizeSession(wallet);
console.log('Conectado:', account.publicKey.toBase58());
});
};
return (
<View>
{selectedAccount ? (
<Text>Conectado: {selectedAccount.publicKey.toBase58()}</Text>
) : (
<Button title="Conectar Carteira" onPress={handleConnect} />
)}
</View>
);
}Enviando Transações com o Provider
Crie um hook customizado para transações que usa o provider:
// hooks/useSendTransaction.ts
import { useCallback } from 'react';
import {
Connection,
PublicKey,
VersionedTransaction,
TransactionMessage,
SystemProgram,
LAMPORTS_PER_SOL,
} from '@solana/web3.js';
import { transact } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import { useAuthorization } from '../providers/AuthorizationProvider';
import { useConnection } from '../providers/ConnectionProvider';
export function useSendSol() {
const { authorizeSession } = useAuthorization();
const { connection } = useConnection();
return useCallback(
async (recipient: PublicKey, amountSol: number): Promise<string> => {
return await transact(async (wallet) => {
// Usar função de autorização do provider
const account = await authorizeSession(wallet);
const { blockhash } = await connection.getLatestBlockhash();
const transaction = new VersionedTransaction(
new TransactionMessage({
payerKey: account.publicKey,
recentBlockhash: blockhash,
instructions: [
SystemProgram.transfer({
fromPubkey: account.publicKey,
toPubkey: recipient,
lamports: amountSol * LAMPORTS_PER_SOL,
}),
],
}).compileToV0Message()
);
const [signature] = await wallet.signAndSendTransactions({
transactions: [transaction],
});
return signature;
});
},
[authorizeSession, connection]
);
}Uso em um componente:
function SendScreen() {
const sendSol = useSendSol();
const [sending, setSending] = useState(false);
const handleSend = async () => {
setSending(true);
try {
const sig = await sendSol(recipientPubkey, 0.1);
Alert.alert('Sucesso', `Transação: ${sig}`);
} catch (e) {
Alert.alert('Erro', e.message);
} finally {
setSending(false);
}
};
return <Button title="Enviar 0.1 SOL" onPress={handleSend} disabled={sending} />;
}Fluxo de Desconexão
Implemente um botão de desconectar:
function DisconnectButton() {
const { selectedAccount, deauthorizeSession } = useAuthorization();
const handleDisconnect = async () => {
await transact(async (wallet) => {
await deauthorizeSession(wallet);
});
};
if (!selectedAccount) return null;
return <Button title="Desconectar" onPress={handleDisconnect} />;
}Após desconectar:
selectedAccounttorna-senullaccountstorna-senullAuth token em cache é limpo
Qualquer componente usando
useAuthorizationre-renderiza
Suporte a Múltiplas Contas
Algumas carteiras autorizam múltiplas contas. O provider lida com isso:
function AccountPicker() {
const { accounts, selectedAccount, onChangeAccount } = useAuthorization();
if (!accounts || accounts.length <= 1) return null;
return (
<View>
<Text>Selecionar Conta:</Text>
{accounts.map((account) => (
<TouchableOpacity
key={account.address}
onPress={() => onChangeAccount(account)}
style={[
styles.accountItem,
account.address === selectedAccount?.address && styles.selected,
]}
>
<Text>{account.label ?? account.publicKey.toBase58().slice(0, 8)}...</Text>
</TouchableOpacity>
))}
</View>
);
}Quando o usuário seleciona uma conta diferente, selectedAccount atualiza e todos os componentes inscritos re-renderizam.
Connection Provider
Para completude, aqui está um ConnectionProvider mínimo:
// src/providers/ConnectionProvider.tsx
import React, { createContext, useContext, useMemo, ReactNode } from 'react';
import { Connection } from '@solana/web3.js';
const RPC_ENDPOINT = 'https://api.devnet.solana.com';
interface ConnectionContextValue {
connection: Connection;
}
const ConnectionContext = createContext<ConnectionContextValue>({
connection: new Connection(RPC_ENDPOINT),
});
export function ConnectionProvider({ children }: { children: ReactNode }) {
const connection = useMemo(
() => new Connection(RPC_ENDPOINT, 'confirmed'),
[]
);
return (
<ConnectionContext.Provider value={{ connection }}>
{children}
</ConnectionContext.Provider>
);
}
export function useConnection(): ConnectionContextValue {
return useContext(ConnectionContext);
}Benefícios do Padrão Provider
Esta arquitetura fornece:
Fonte única de verdade: Um lugar gerencia o estado da carteira
Cache automático de tokens: Componentes não lidam com AsyncStorage
Atualizações reativas: React re-renderiza quando o estado de auth muda
Separação de responsabilidades: Componentes de UI não contêm lógica MWA
Testabilidade: Faça mock do contexto para testes unitários
Segurança de tipos: TypeScript garante uso correto
O padrão provider é padrão em apps React Native profissionais. Na próxima lição, construiremos sobre esta fundação com tratamento de erros abrangente.