此內容正在翻譯中,準備好後將在此處提供。
Conclusion

You've learned the Mobile Wallet Adapter protocol from first principles to production patterns. This final lesson consolidates everything into a complete token sender application and sets you up for the next steps in Solana mobile development.
Capstone Project: Token Sender
Let's build a complete dApp that:
Connects to a wallet using the AuthorizationProvider
Displays account balance
Sends SOL to any address
Shows transaction status with proper error handling
Project Structure
src/
├── App.tsx
├── providers/
│ ├── AuthorizationProvider.tsx
│ └── ConnectionProvider.tsx
├── hooks/
│ ├── useBalance.ts
│ └── useSendSol.ts
├── screens/
│ └── SendScreen.tsx
├── components/
│ ├── ConnectButton.tsx
│ ├── BalanceDisplay.tsx
│ └── TransactionStatus.tsx
└── utils/
└── mwaErrorHandler.tsThe Main App
The app entry point wraps everything in the necessary providers. We use react-native-safe-area-context for proper safe area handling across different devices.
// App.tsx
import './src/polyfills'; // Load polyfills first
import React from 'react';
import { StyleSheet, StatusBar } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { ConnectionProvider } from './src/providers/ConnectionProvider';
import { AuthorizationProvider } from './src/providers/AuthorizationProvider';
import { SendScreen } from './src/screens/SendScreen';
export default function App() {
return (
<SafeAreaProvider>
<ConnectionProvider>
<AuthorizationProvider>
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
<SendScreen />
</SafeAreaView>
</AuthorizationProvider>
</ConnectionProvider>
</SafeAreaProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0a0a0a',
},
});Balance Hook
This hook fetches the connected wallet's SOL balance and refreshes it periodically.
// hooks/useBalance.ts
import { useState, useEffect, useCallback } from 'react';
import { LAMPORTS_PER_SOL } from '@solana/web3.js';
import { useConnection } from '../providers/ConnectionProvider';
import { useAuthorization } from '../providers/AuthorizationProvider';
export function useBalance() {
const { connection } = useConnection();
const { selectedAccount } = useAuthorization();
const [balance, setBalance] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const fetchBalance = useCallback(async () => {
if (!selectedAccount) {
setBalance(null);
return;
}
setLoading(true);
try {
const lamports = await connection.getBalance(selectedAccount.publicKey);
setBalance(lamports / LAMPORTS_PER_SOL);
} catch (error) {
console.error('Failed to fetch balance:', error);
setBalance(null);
} finally {
setLoading(false);
}
}, [connection, selectedAccount]);
useEffect(() => {
fetchBalance();
// Refresh balance every 30 seconds
const interval = setInterval(fetchBalance, 30000);
return () => clearInterval(interval);
}, [fetchBalance]);
return { balance, loading, refresh: fetchBalance };
}Send SOL Hook
// hooks/useSendSol.ts
import { useState, useCallback } from 'react';
import {
PublicKey,
VersionedTransaction,
TransactionMessage,
SystemProgram,
LAMPORTS_PER_SOL,
} from '@solana/web3.js';
import { transact } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import { useConnection } from '../providers/ConnectionProvider';
import { useAuthorization } from '../providers/AuthorizationProvider';
import { handleMWAError, MWAErrorResult } from '../utils/mwaErrorHandler';
type SendStatus = 'idle' | 'building' | 'signing' | 'confirming' | 'success' | 'error';
interface SendResult {
status: SendStatus;
signature: string | null;
error: string | null;
}
export function useSendSol() {
const { connection } = useConnection();
const { authorizeSession } = useAuthorization();
const [result, setResult] = useState<SendResult>({
status: 'idle',
signature: null,
error: null,
});
const send = useCallback(
async (recipientAddress: string, amountSol: number): Promise<boolean> => {
// Reset state
setResult({ status: 'building', signature: null, error: null });
try {
// Validate recipient address
let recipientPubkey: PublicKey;
try {
recipientPubkey = new PublicKey(recipientAddress);
} catch {
setResult({ status: 'error', signature: null, error: 'Invalid recipient address' });
return false;
}
// Validate amount
if (amountSol <= 0) {
setResult({ status: 'error', signature: null, error: 'Amount must be greater than 0' });
return false;
}
const signature = await transact(async (wallet) => {
setResult((prev) => ({ ...prev, status: 'signing' }));
const account = await authorizeSession(wallet);
// Get fresh blockhash
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash('confirmed');
// Build transaction
const transaction = new VersionedTransaction(
new TransactionMessage({
payerKey: account.publicKey,
recentBlockhash: blockhash,
instructions: [
SystemProgram.transfer({
fromPubkey: account.publicKey,
toPubkey: recipientPubkey,
lamports: Math.floor(amountSol * LAMPORTS_PER_SOL),
}),
],
}).compileToV0Message()
);
// Sign and send
const [sig] = await wallet.signAndSendTransactions({
transactions: [transaction],
});
setResult((prev) => ({ ...prev, status: 'confirming', signature: sig }));
// Wait for confirmation
await connection.confirmTransaction(
{ signature: sig, blockhash, lastValidBlockHeight },
'confirmed'
);
return sig;
});
setResult({ status: 'success', signature, error: null });
return true;
} catch (error) {
const mwaError = handleMWAError(error);
// Don't show error for user cancellation
if (mwaError.isUserCancellation) {
setResult({ status: 'idle', signature: null, error: null });
return false;
}
setResult({ status: 'error', signature: null, error: mwaError.userMessage });
return false;
}
},
[connection, authorizeSession]
);
const reset = useCallback(() => {
setResult({ status: 'idle', signature: null, error: null });
}, []);
return { send, reset, ...result };
}Send Screen
// screens/SendScreen.tsx
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Linking,
} from 'react-native';
import { useAuthorization } from '../providers/AuthorizationProvider';
import { useBalance } from '../hooks/useBalance';
import { useSendSol } from '../hooks/useSendSol';
import { ConnectButton } from '../components/ConnectButton';
export function SendScreen() {
const { selectedAccount } = useAuthorization();
const { balance, loading: balanceLoading, refresh } = useBalance();
const { send, status, signature, error, reset } = useSendSol();
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const handleSend = async () => {
const amountNum = parseFloat(amount);
if (isNaN(amountNum)) return;
const success = await send(recipient, amountNum);
if (success) {
setRecipient('');
setAmount('');
refresh(); // Refresh balance after send
}
};
const openExplorer = () => {
if (signature) {
Linking.openURL(`https://explorer.solana.com/tx/${signature}?cluster=devnet`);
}
};
// Not connected state
if (!selectedAccount) {
return (
<View style={styles.centered}>
<Text style={styles.title}>Token Sender</Text>
<Text style={styles.subtitle}>Connect your wallet to send SOL</Text>
<ConnectButton />
</View>
);
}
// Connected state
return (
<View style={styles.container}>
<Text style={styles.title}>Token Sender</Text>
{/* Account Info */}
<View style={styles.card}>
<Text style={styles.label}>Connected Account</Text>
<Text style={styles.address}>
{selectedAccount.publicKey.toBase58().slice(0, 20)}...
</Text>
<Text style={styles.balance}>
{balanceLoading ? 'Loading...' : `${balance?.toFixed(4) ?? '—'} SOL`}
</Text>
</View>
{/* Send Form */}
<View style={styles.card}>
<Text style={styles.label}>Recipient Address</Text>
<TextInput
style={styles.input}
value={recipient}
onChangeText={setRecipient}
placeholder="Enter Solana address"
placeholderTextColor="#666"
autoCapitalize="none"
autoCorrect={false}
/>
<Text style={styles.label}>Amount (SOL)</Text>
<TextInput
style={styles.input}
value={amount}
onChangeText={setAmount}
placeholder="0.0"
placeholderTextColor="#666"
keyboardType="decimal-pad"
/>
<TouchableOpacity
style={[styles.button, status !== 'idle' && status !== 'error' && styles.buttonDisabled]}
onPress={handleSend}
disabled={status !== 'idle' && status !== 'error'}
>
<Text style={styles.buttonText}>
{status === 'building' && 'Building...'}
{status === 'signing' && 'Approve in Wallet...'}
{status === 'confirming' && 'Confirming...'}
{(status === 'idle' || status === 'error' || status === 'success') && 'Send SOL'}
</Text>
</TouchableOpacity>
</View>
{/* Status */}
{status === 'success' && signature && (
<View style={styles.successCard}>
<Text style={styles.successText}>Transaction confirmed!</Text>
<TouchableOpacity onPress={openExplorer}>
<Text style={styles.link}>View on Explorer →</Text>
</TouchableOpacity>
</View>
)}
{status === 'error' && error && (
<View style={styles.errorCard}>
<Text style={styles.errorText}>{error}</Text>
<TouchableOpacity onPress={reset}>
<Text style={styles.link}>Dismiss</Text>
</TouchableOpacity>
</View>
)}
{/* Disconnect */}
<ConnectButton />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 16,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#fff',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#888',
marginBottom: 24,
},
card: {
backgroundColor: '#1a1a1a',
borderRadius: 12,
padding: 16,
marginBottom: 16,
},
label: {
fontSize: 12,
color: '#888',
marginBottom: 4,
textTransform: 'uppercase',
},
address: {
fontSize: 14,
color: '#fff',
fontFamily: 'monospace',
},
balance: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f0',
marginTop: 8,
},
input: {
backgroundColor: '#2a2a2a',
borderRadius: 8,
padding: 12,
color: '#fff',
fontSize: 16,
marginBottom: 16,
},
button: {
backgroundColor: '#512da8',
borderRadius: 8,
padding: 16,
alignItems: 'center',
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
successCard: {
backgroundColor: '#1a3a1a',
borderRadius: 12,
padding: 16,
marginBottom: 16,
},
successText: {
color: '#0f0',
fontSize: 16,
marginBottom: 8,
},
errorCard: {
backgroundColor: '#3a1a1a',
borderRadius: 12,
padding: 16,
marginBottom: 16,
},
errorText: {
color: '#f55',
fontSize: 16,
marginBottom: 8,
},
link: {
color: '#88f',
fontSize: 14,
},
});Connect Button Component
// components/ConnectButton.tsx
import React from 'react';
import { TouchableOpacity, Text, StyleSheet, Alert } from 'react-native';
import { transact } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import { useAuthorization } from '../providers/AuthorizationProvider';
import { handleMWAError } from '../utils/mwaErrorHandler';
export function ConnectButton() {
const { selectedAccount, authorizeSession, deauthorizeSession } = useAuthorization();
const handleConnect = async () => {
try {
await transact(async (wallet) => {
await authorizeSession(wallet);
});
} catch (error) {
const mwaError = handleMWAError(error);
if (!mwaError.isUserCancellation) {
Alert.alert('Connection Error', mwaError.userMessage);
}
}
};
const handleDisconnect = async () => {
try {
await transact(async (wallet) => {
await deauthorizeSession(wallet);
});
} catch (error) {
console.log('Disconnect error:', error);
}
};
if (selectedAccount) {
return (
<TouchableOpacity style={styles.disconnectButton} onPress={handleDisconnect}>
<Text style={styles.disconnectText}>Disconnect Wallet</Text>
</TouchableOpacity>
);
}
return (
<TouchableOpacity style={styles.connectButton} onPress={handleConnect}>
<Text style={styles.connectText}>Connect Wallet</Text>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
connectButton: {
backgroundColor: '#512da8',
borderRadius: 8,
padding: 16,
alignItems: 'center',
},
connectText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
disconnectButton: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: '#666',
borderRadius: 8,
padding: 12,
alignItems: 'center',
},
disconnectText: {
color: '#888',
fontSize: 14,
},
});What You've Learned
This course covered:
Protocol Fundamentals: How MWA sessions work, the local socket transport, ECDH key exchange, and encrypted JSON-RPC.
Environment Setup: React Native project configuration, required polyfills, Expo development builds.
Wallet Connection: The
transact()pattern, authorization flow, identity verification, auth token caching.Transaction Signing: Building versioned transactions,
signAndSendTransactionsvssignTransactions, batching strategies.Message Signing: Off-chain attestations, Sign In With Solana, backend verification.
State Management: AuthorizationProvider pattern, React Context for wallet state, multi-account support.
Error Handling: MWA error codes, user-friendly messages, retry strategies, session recovery.
Device Testing: Android setup, debugging workflows, testing checklist.
Next Steps
You're now equipped to build production mobile Solana dApps. Here's where to go next:
Advanced MWA Features:
Wallet capabilities detection
Custom chain identifiers
Token account management
Beyond MWA:
Solana Pay for payments and requests
Blinks and Actions for shareable transactions
Secp256r1 for passkey authentication
Production Considerations:
App Store submission requirements
Analytics and crash reporting
Security audits for wallet interactions
The mobile Solana ecosystem is growing rapidly. With MWA as your foundation, you can build apps that bring self-custody to billions of mobile users.
Congratulations on completing the Mobile Wallet Adapter course!