Solana Mobile Development: Building Apps for Saga and Seeker
**Category:** Development Tutorial | **Audience:** Intermediate Developers
**Reading Time:** 16 minutes | **Author:** Builderz Team
**Verified Packages:** @solana-mobile/mobile-wallet-adapter-protocol@2.2.5, @solana/web3.js@1.98.4
What You'll Learn
The Solana Mobile Stack (SMS) enables native mobile applications with deep blockchain integration for the Saga and Seeker devices. This tutorial teaches you to build Android apps with Mobile Wallet Adapter, secure key management via Seed Vault, and Solana Pay integration. By the end, you'll have a complete mobile dApp ready for the Solana dApp Store.
Key Takeaways:
- Understand the Solana Mobile Stack architecture
- Implement Mobile Wallet Adapter for transaction signing
- Use Seed Vault for secure key management
- Build for both React Native and native Android
- Submit to the Solana dApp Store
TL;DR
Solana Mobile Stack brings blockchain-native features to Android:
- **Mobile Wallet Adapter**: Universal protocol for wallet integration
- **Seed Vault**: Secure enclave for key storage
- **dApp Store**: Crypto-native app distribution
- **Devices**: Saga (launched 2023), Seeker (shipping 2025, 140k+ preorders)
// Quick start: Connect mobile wallet
import { MobileWalletAdapter } from '@solana-mobile/mobile-wallet-adapter-protocol';
const adapter = new MobileWalletAdapter();
await adapter.connect();
const publicKey = adapter.publicKey;Prerequisites
Before starting, ensure you have:
- **Android Studio** Arctic Fox or newer
- **React Native 0.73+** (for React Native apps) or Kotlin (for native)
- **Solana Saga/Seeker** device or Android emulator
- Understanding of **Solana transactions**
- Basic **React Native** or Android development experience
# React Native project setup
npx react-native init SolanaMobileApp --template react-native-template-typescript
cd SolanaMobileApp
npm install @solana-mobile/mobile-wallet-adapter-protocol @solana/web3.jsUnderstanding the Solana Mobile Stack
Core Components
The Solana Mobile Stack consists of four integrated components:
| Component | Purpose | Key Feature |
|-----------|---------|-------------|
| Mobile Wallet Adapter | Wallet connection protocol | Universal wallet compatibility |
| Seed Vault | Secure key storage | Hardware-backed security |
| dApp Store | App distribution | Crypto-native discovery |
| Solana Pay | Payment integration | QR code transactions |
Device Landscape
Solana Saga (2023)
- First Solana-native smartphone
- Qualcomm Snapdragon 8+ Gen 1
- Secure element for Seed Vault
- 512GB storage, 12GB RAM
Solana Seeker (2025)
- More affordable entry point (~$500)
- Genesis Token (soulbound NFT) included
- Improved dApp Store 2.0
- 140,000+ preorders from 57 countries
**Builderz Insight**: While developing for specific devices, Mobile Wallet Adapter works on any Android phone with a compatible wallet (Phantom, Solflare, etc.), expanding your potential user base significantly.
Project Setup
React Native Configuration
# Create new React Native project
npx react-native init SolanaApp --template react-native-template-typescript
cd SolanaApp
# Install Solana Mobile dependencies
npm install @solana-mobile/mobile-wallet-adapter-protocol
npm install @solana-mobile/mobile-wallet-adapter-protocol-web3js
npm install @solana/web3.js
npm install react-native-get-random-values
npm install buffer
# For iOS (coming soon) and polyfills
npm install @react-native-async-storage/async-storageConfigure Metro for Crypto Libraries
// metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const config = {
resolver: {
extraNodeModules: {
crypto: require.resolve('react-native-crypto'),
stream: require.resolve('readable-stream'),
buffer: require.resolve('buffer'),
},
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);Polyfill Setup
// index.js or App.tsx (at the very top)
import 'react-native-get-random-values';
import { Buffer } from 'buffer';
global.Buffer = Buffer;Implementing Mobile Wallet Adapter
Basic Wallet Connection
// src/hooks/useMobileWallet.ts
import { useState, useCallback } from 'react';
import {
transact,
Web3MobileWallet,
} from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import { Connection, PublicKey, Transaction, clusterApiUrl } from '@solana/web3.js';
const APP_IDENTITY = {
name: 'My Solana App',
uri: 'https://mysolanaapp.com',
icon: 'favicon.ico',
};
export function useMobileWallet() {
const [publicKey, setPublicKey] = useState<PublicKey | null>(null);
const [connected, setConnected] = useState(false);
const connect = useCallback(async () => {
try {
const authResult = await transact(async (wallet: Web3MobileWallet) => {
// Request authorization
const authorizationResult = await wallet.authorize({
cluster: 'mainnet-beta',
identity: APP_IDENTITY,
});
return authorizationResult;
});
const userPublicKey = new PublicKey(authResult.accounts[0].address);
setPublicKey(userPublicKey);
setConnected(true);
console.log('Connected:', userPublicKey.toBase58());
return userPublicKey;
} catch (error) {
console.error('Connection failed:', error);
throw error;
}
}, []);
const disconnect = useCallback(async () => {
try {
await transact(async (wallet: Web3MobileWallet) => {
await wallet.deauthorize({
auth_token: '', // Stored from previous authorization
});
});
setPublicKey(null);
setConnected(false);
} catch (error) {
console.error('Disconnect failed:', error);
}
}, []);
return {
publicKey,
connected,
connect,
disconnect,
};
}Transaction Signing
// src/hooks/useTransactionSigner.ts
import { useCallback } from 'react';
import {
transact,
Web3MobileWallet,
} from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import {
Connection,
Transaction,
SystemProgram,
PublicKey,
LAMPORTS_PER_SOL,
clusterApiUrl,
} from '@solana/web3.js';
const connection = new Connection(clusterApiUrl('mainnet-beta'), 'confirmed');
export function useTransactionSigner() {
const signAndSendTransaction = useCallback(
async (transaction: Transaction, feePayer: PublicKey): Promise<string> => {
try {
// Get latest blockhash
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.lastValidBlockHeight = lastValidBlockHeight;
transaction.feePayer = feePayer;
// Sign and send via Mobile Wallet Adapter
const signature = await transact(async (wallet: Web3MobileWallet) => {
// Reauthorize if needed
await wallet.authorize({
cluster: 'mainnet-beta',
identity: {
name: 'My Solana App',
uri: 'https://mysolanaapp.com',
},
});
// Sign and send transaction
const signedTx = await wallet.signAndSendTransactions({
transactions: [transaction],
});
return signedTx[0];
});
console.log('Transaction sent:', signature);
// Wait for confirmation
const confirmation = await connection.confirmTransaction({
signature,
blockhash,
lastValidBlockHeight,
});
if (confirmation.value.err) {
throw new Error(`Transaction failed: ${confirmation.value.err}`);
}
return signature;
} catch (error) {
console.error('Transaction failed:', error);
throw error;
}
},
[]
);
const sendSol = useCallback(
async (from: PublicKey, to: PublicKey, amount: number): Promise<string> => {
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: from,
toPubkey: to,
lamports: amount * LAMPORTS_PER_SOL,
})
);
return signAndSendTransaction(transaction, from);
},
[signAndSendTransaction]
);
return {
signAndSendTransaction,
sendSol,
};
}Building the UI Components
Wallet Connect Button
// src/components/WalletButton.tsx
import React from 'react';
import { TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { useMobileWallet } from '../hooks/useMobileWallet';
export function WalletButton() {
const { connected, publicKey, connect, disconnect } = useMobileWallet();
const [loading, setLoading] = React.useState(false);
const handlePress = async () => {
setLoading(true);
try {
if (connected) {
await disconnect();
} else {
await connect();
}
} catch (error) {
console.error('Wallet action failed:', error);
} finally {
setLoading(false);
}
};
const displayAddress = publicKey
? `${publicKey.toBase58().slice(0, 4)}...${publicKey.toBase58().slice(-4)}`
: '';
return (
<TouchableOpacity
style={[styles.button, connected && styles.connected]}
onPress={handlePress}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.text}>
{connected ? displayAddress : 'Connect Wallet'}
</Text>
)}
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#6B5CFF', // builderz-purple
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
connected: {
backgroundColor: '#4a4a4a',
},
text: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});Send SOL Form
// src/components/SendSolForm.tsx
import React, { useState } from 'react';
import { View, TextInput, Text, TouchableOpacity, StyleSheet, Alert } from 'react-native';
import { PublicKey } from '@solana/web3.js';
import { useMobileWallet } from '../hooks/useMobileWallet';
import { useTransactionSigner } from '../hooks/useTransactionSigner';
export function SendSolForm() {
const { publicKey, connected } = useMobileWallet();
const { sendSol } = useTransactionSigner();
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const [sending, setSending] = useState(false);
const handleSend = async () => {
if (!publicKey || !recipient || !amount) return;
try {
setSending(true);
const recipientPubkey = new PublicKey(recipient);
const solAmount = parseFloat(amount);
if (isNaN(solAmount) || solAmount <= 0) {
Alert.alert('Error', 'Invalid amount');
return;
}
const signature = await sendSol(publicKey, recipientPubkey, solAmount);
Alert.alert(
'Success!',
`Sent ${solAmount} SOL\n\nSignature: ${signature.slice(0, 20)}...`
);
setRecipient('');
setAmount('');
} catch (error) {
Alert.alert('Error', error instanceof Error ? error.message : 'Transaction failed');
} finally {
setSending(false);
}
};
if (!connected) {
return (
<View style={styles.container}>
<Text style={styles.message}>Connect wallet to send SOL</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.label}>Recipient Address</Text>
<TextInput
style={styles.input}
placeholder="Enter Solana address"
placeholderTextColor="#666"
value={recipient}
onChangeText={setRecipient}
autoCapitalize="none"
autoCorrect={false}
/>
<Text style={styles.label}>Amount (SOL)</Text>
<TextInput
style={styles.input}
placeholder="0.00"
placeholderTextColor="#666"
value={amount}
onChangeText={setAmount}
keyboardType="decimal-pad"
/>
<TouchableOpacity
style={[styles.sendButton, sending && styles.disabled]}
onPress={handleSend}
disabled={sending || !recipient || !amount}
>
<Text style={styles.sendText}>
{sending ? 'Sending...' : 'Send SOL'}
</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 16,
},
label: {
color: '#fff',
fontSize: 14,
marginBottom: 8,
},
input: {
backgroundColor: '#2a2a2a',
borderRadius: 8,
padding: 12,
color: '#fff',
marginBottom: 16,
},
sendButton: {
backgroundColor: '#C7FF4D', // builderz-acid
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
disabled: {
opacity: 0.5,
},
sendText: {
color: '#000',
fontSize: 16,
fontWeight: '700',
},
message: {
color: '#666',
textAlign: 'center',
},
});Native Android Development (Kotlin)
For native Android apps, use the Kotlin SDK directly.
Gradle Configuration
// app/build.gradle
dependencies {
implementation 'com.solana:mobile-wallet-adapter:2.2.5'
implementation 'org.sol4k:sol4k:0.4.2' // Kotlin Solana SDK
}Native Wallet Connection
// WalletManager.kt
import com.solana.mobilewalletadapter.clientlib.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class WalletManager(private val activity: Activity) {
private val _publicKey = MutableStateFlow<String?>(null)
val publicKey: StateFlow<String?> = _publicKey
private val _connected = MutableStateFlow(false)
val connected: StateFlow<Boolean> = _connected
private val identityUri = Uri.parse("https://mysolanaapp.com")
private val iconUri = Uri.parse("favicon.ico")
private val identityName = "My Solana App"
suspend fun connect() {
try {
val result = MobileWalletAdapter.transact(activity) { sender ->
val authorizationResult = authorize(
identityUri = identityUri,
iconUri = iconUri,
identityName = identityName,
cluster = RpcCluster.MainnetBeta
)
authorizationResult
}
result.accounts.firstOrNull()?.let { account ->
_publicKey.value = account.publicKey.toBase58()
_connected.value = true
}
} catch (e: Exception) {
Log.e("WalletManager", "Connection failed", e)
throw e
}
}
suspend fun signAndSendTransaction(transaction: Transaction): String {
return MobileWalletAdapter.transact(activity) { sender ->
val signedTransactions = signAndSendTransactions(
transactions = listOf(transaction.serialize())
)
signedTransactions.first()
}
}
}Compose UI
// WalletScreen.kt
@Composable
fun WalletScreen(walletManager: WalletManager) {
val publicKey by walletManager.publicKey.collectAsState()
val connected by walletManager.connected.collectAsState()
val scope = rememberCoroutineScope()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = {
scope.launch {
if (connected) {
walletManager.disconnect()
} else {
walletManager.connect()
}
}
},
colors = ButtonDefaults.buttonColors(
containerColor = if (connected) Color.Gray else Color(0xFF6B5CFF)
)
) {
Text(
text = if (connected) {
publicKey?.take(8) + "..." ?: "Connected"
} else {
"Connect Wallet"
}
)
}
Spacer(modifier = Modifier.height(24.dp))
if (connected) {
SendSolForm(walletManager = walletManager)
}
}
}Seed Vault Integration
Seed Vault provides hardware-backed key storage on Saga and Seeker devices.
Checking Seed Vault Availability
// src/utils/seedVault.ts
import { SeedVault } from '@solana-mobile/seed-vault';
export async function isSeedVaultAvailable(): Promise<boolean> {
try {
const available = await SeedVault.isAvailable();
return available;
} catch {
return false;
}
}
export async function requestSeedVaultPermission(): Promise<boolean> {
try {
const granted = await SeedVault.requestPermission();
return granted;
} catch (error) {
console.error('Seed Vault permission denied:', error);
return false;
}
}Using Seed Vault for Signing
// When Seed Vault is available, transactions are automatically
// routed through the secure element. The MWA handles this internally.
// For explicit Seed Vault operations:
import { SeedVault } from '@solana-mobile/seed-vault';
export async function signWithSeedVault(
message: Uint8Array
): Promise<Uint8Array> {
const available = await SeedVault.isAvailable();
if (!available) {
throw new Error('Seed Vault not available on this device');
}
const signature = await SeedVault.signMessage(message);
return signature;
}Solana Pay Integration
Enable QR code payments in your mobile app.
// src/hooks/useSolanaPay.ts
import { useCallback } from 'react';
import {
encodeURL,
parseURL,
validateTransfer,
TransferRequestURL,
} from '@solana/pay';
import { Connection, PublicKey, clusterApiUrl } from '@solana/web3.js';
const connection = new Connection(clusterApiUrl('mainnet-beta'));
export function useSolanaPay() {
const generatePaymentURL = useCallback(
(recipient: PublicKey, amount: number, label?: string, message?: string) => {
const url = encodeURL({
recipient,
amount: BigInt(amount * 1e9), // Convert to lamports
label,
message,
});
return url.toString();
},
[]
);
const parsePaymentURL = useCallback(async (url: string) => {
try {
const parsed = parseURL(url);
if ('link' in parsed) {
// Transaction request URL
console.log('Transaction request:', parsed.link);
return { type: 'transaction', data: parsed };
} else {
// Transfer request URL
console.log('Transfer request:', parsed);
return { type: 'transfer', data: parsed };
}
} catch (error) {
console.error('Invalid Solana Pay URL:', error);
throw error;
}
}, []);
const validatePayment = useCallback(
async (signature: string, expectedRecipient: PublicKey, expectedAmount: number) => {
try {
const response = await validateTransfer(
connection,
signature,
{
recipient: expectedRecipient,
amount: BigInt(expectedAmount * 1e9),
},
{ commitment: 'confirmed' }
);
return response;
} catch (error) {
console.error('Payment validation failed:', error);
return false;
}
},
[]
);
return {
generatePaymentURL,
parsePaymentURL,
validatePayment,
};
}Publishing to Solana dApp Store
Store Requirements
The Solana dApp Store has specific requirements:
- **Android APK/AAB** - Standard Android package
- **App manifest** - Solana-specific metadata
- **Publisher wallet** - Verified Solana address
- **Screenshots** - Store listing assets
Create Store Manifest
{
"name": "My Solana App",
"short_description": "Send and receive SOL on mobile",
"long_description": "A mobile-first Solana wallet app...",
"website": "https://mysolanaapp.com",
"category": "finance",
"platforms": {
"android": {
"min_sdk": 26,
"package_name": "com.mysolanaapp"
}
},
"release": {
"address": "YourPublisherWalletAddress",
"catalog_item_id": "unique-catalog-id"
}
}Submission Process
# Install dApp Store CLI
npm install -g @solana-mobile/dapp-store-cli
# Initialize publisher
dapp-store init
# Submit app
dapp-store submit \
--apk ./app-release.apk \
--manifest ./store-manifest.json \
--keypair ./publisher-wallet.jsonCommon Pitfalls
Missing Polyfills
Problem: Crypto functions fail on React Native.
// Solution: Add polyfills at app entry point
import 'react-native-get-random-values';
import { Buffer } from 'buffer';
global.Buffer = Buffer;Session Management
Problem: Authorization expires between app restarts.
// Store auth token securely
import AsyncStorage from '@react-native-async-storage/async-storage';
async function saveAuthToken(token: string) {
await AsyncStorage.setItem('mwa_auth_token', token);
}
async function getAuthToken(): Promise<string | null> {
return AsyncStorage.getItem('mwa_auth_token');
}Wallet Not Found
Problem: No compatible wallet installed.
// Check for installed wallets first
const wallets = await MobileWalletAdapter.getInstalledWallets();
if (wallets.length === 0) {
Alert.alert(
'No Wallet Found',
'Please install Phantom or Solflare to continue',
[{ text: 'OK' }]
);
return;
}Transaction Timeout
Problem: User takes too long to approve in wallet.
// Implement timeout handling
const TRANSACTION_TIMEOUT = 60000; // 60 seconds
try {
const result = await Promise.race([
signAndSendTransaction(tx),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Transaction timeout')), TRANSACTION_TIMEOUT)
),
]);
} catch (error) {
if (error.message === 'Transaction timeout') {
Alert.alert('Timeout', 'Transaction approval took too long. Please try again.');
}
}Next Steps & Resources
Advanced Topics
- **NFT Minting**: Integrate Metaplex for mobile NFT creation
- **DeFi Integration**: Jupiter swaps on mobile
- **Push Notifications**: Alert users of on-chain events
- **Biometric Auth**: Face ID / fingerprint for transactions
Official Resources
- [Solana Mobile Docs](https://docs.solanamobile.com/)
- [Mobile Wallet Adapter GitHub](https://github.com/solana-mobile/mobile-wallet-adapter)
- [Solana dApp Store](https://solanamobile.com/dapp-store)
- [SMS SDK Reference](https://github.com/solana-mobile/solana-mobile-stack-sdk)
Related Builderz Content
- [Jupiter Token Swap Tutorial](/blog/jupiter-token-swap-tutorial)
- [Solana Development Services](/services)
- [Contact Us](/contact)
Summary
Solana Mobile development opens new possibilities for crypto applications:
- **Mobile Wallet Adapter**: Universal wallet protocol for any Android app
- **Seed Vault**: Hardware-backed security on Saga/Seeker devices
- **dApp Store**: Crypto-native distribution channel
- **Cross-platform**: React Native or native Kotlin development
With 140,000+ Seeker preorders and growing ecosystem support, mobile is becoming a primary interface for Solana applications.
Built with expertise from Builderz - 32+ Solana projects including mobile-first applications.



