Mobile
移動錢包適配器

移動錢包適配器

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

Conclusion

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.

The best way to internalize what you've learned is to build something real.

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

text
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.ts

The 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.

Important: Install the safe area context package before proceeding: npx expo install react-native-safe-area-context

typescript
// 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.

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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:

  1. Protocol Fundamentals: How MWA sessions work, the local socket transport, ECDH key exchange, and encrypted JSON-RPC.

  2. Environment Setup: React Native project configuration, required polyfills, Expo development builds.

  3. Wallet Connection: The transact() pattern, authorization flow, identity verification, auth token caching.

  4. Transaction Signing: Building versioned transactions, signAndSendTransactions vs signTransactions, batching strategies.

  5. Message Signing: Off-chain attestations, Sign In With Solana, backend verification.

  6. State Management: AuthorizationProvider pattern, React Context for wallet state, multi-account support.

  7. Error Handling: MWA error codes, user-friendly messages, retry strategies, session recovery.

  8. 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!

恭喜,您已完成此課程系列!
Blueshift © 2026Commit: 1b8118f