13 min read
Overview
Solana mobile apps are growing fast, fueled by the Seeker phone and Solana dApp Store, which give developers direct access to an audience of native crypto users. By learning to build and connect Solana apps for mobile now, you can ship faster and capture users early as the Solana mobile ecosystem expands.
In this guide, you’ll build a lightweight Solana Android app using React Native, Expo, Solana Kit, and Mobile Wallet Adapter (MWA) to connect a Mock Wallet, request an airdrop, and transfer SOL.
You’ll come away with a strong foundation in Solana mobile development that you can later expand into a complete production-ready app.
What You Will Do
- Set up Android Studio and an emulator
- Scaffold a React Native app with Expo
- Add Mobile Wallet Adapter (MWA) at the native Android layer
- Install and run the Mock Wallet for authorization and signing
- Build wallet flows: Connect → Balance → Airdrop → Send SOL
- Test end-to-end on the emulator
What You Will Need
This guide assumes familiarity with Solana programming, React, and TypeScript, as well as a general understanding of:
- Solana Fundamentals
- The Solana Mobile Stack
- How to Develop for Android
- Building apps with React Native
- Using the Expo CLI
| Dependency | Version |
|---|---|
| Android Studio | Narwhal 4 2025.1.4 |
| AVD (emulator) | Android 16, API 36.1 |
| Node | 24.8.0 |
| Expo | 54.0.13 |
| @solana/kit | 3.0.3 |
| Java JDK | 17 |
Install Android Studio
MWA uses Android’s native SDKs, so you’ll build with Android Studio and run on an emulator. If you don’t have it yet, install Android Studio and launch it.
React Native/Expo’s Android Gradle Plugin requires JDK 17:
- In Android Studio, open Settings/Preferences → Build, Execution, Deployment → Build Tools → Gradle
- In Gradle JDK options, select a JDK 17 installation. If JDK 17 isn’t listed, install it, then point Android Studio to that path.
- Set your shell to match Studio:
JAVA_HOME→ JDK 17 installation (Optional, but recommended) - Create an Android Virtual Device (AVD) in the Device Manager.
- Let Android Studio install any required SDKs/Build Tools and finish syncing.
Add PIN to the Emulator
To use the Mock Wallet, set a screen PIN so the device is secure and can approve sessions and signatures.
- Boot the emulator: Start your Android 16 (API 36) AVD (Medium Phone profile) from Android Studio.
- Set a screen lock (required): Open Settings → Security & privacy → Device Unlock -> Screen lock → set a PIN.
Install Mock Wallet
Mock Wallet lets you authorize and sign on Android without a production wallet.
Mock Wallet is not a secure, end-user wallet and should not be used in production or with real funds.
Clone the Repo:
git clone https://github.com/solana-mobile/mock-mwa-wallet.git
In Android Studio, open the mock-mwa-wallet/build.gradle file, let Gradle sync, then run it on an emulator.
Open Mock MWA Wallet on the emulator. When prompted inside the wallet, tap Authenticate to enable signing for development sessions.
Solana Android App Overview
We’ll build a React Native Android app using Expo that is integrated with MWA for core Solana flows:
- Connect/disconnect wallet
- Show SOL balance
- Request a devnet airdrop
- Send SOL to a recipient
MWA allows Android apps to connect with any MWA-compliant wallet on the device, eliminating the need to integrate each wallet individually. To integrate MWA, we’ll use React Native’s bare workflow. Expo CLI will only be used for scaffolding, build/config helpers, and asset handling.
We’ll include @solana/web3.js to satisfy MWA’s expected types, but implement airdrop and send logic with @solana/kit.
App Structure
We split code by responsibility (UI → hooks → services) to keep it simple, testable, and scalable, so native wallet/intents stay isolated and making features easier to add and reuse.
src/
├─ index.tsx
├─ components/
│ ├─ BalanceDisplay.tsx
│ ├─ ConnectScreen.tsx
│ ├─ DisconnectModal.tsx
│ ├─ ErrorModal.tsx
│ ├─ Header.tsx
│ └─ SendSolForm.tsx
├─ constants/
│ └─ index.ts
├─ hooks/
│ ├─ useAirdrop.ts
│ └─ useSolBalance.ts
├─ services/
│ ├─ airdropService.ts
│ ├─ solanaService.ts
│ ├─ transferService.ts
│ └─ walletService.ts
├─ styles/
│ └─ index.ts
└─ utils/
└─ addressUtils.ts
To get started, clone the sample app repository and open the Solana mobile app folder:
git clone https://github.com/quiknode-labs/qn-guide-examples.git
cd qn-guide-examples/solana/solana-mobile-app
Let’s explore the core service files behind our app and the responsibilities they cover.
Solana Service
solanaService.ts handles basic Solana RPC operations (balance fetching). It does this by calling getBalance to fetch lamports, and converting to SOL for display.
import { address } from '@solana/kit';
import type { RpcClient } from './rpcClient';
export const fetchSolBalance = async (
addressString: string,
rpc: RpcClient
): Promise<number> => {
try {
console.log('Fetching balance for address:', addressString);
// Convert string address to address type
const solanaAddress = address(addressString);
// Get balance using the proper @solana/kit API
const { value: balanceLamports } = await rpc.getBalance(solanaAddress).send();
console.log('Balance in lamports:', balanceLamports);
return Number(balanceLamports);
} catch (error) {
// ...
}
};
Wallet Service
walletService.ts handles connecting to a mobile wallet by using MWA to authorize, saving the returned auth_token, and returning the wallet address.
import { transact, Web3MobileWallet } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import { APP_IDENTITY } from '../constants';
import AsyncStorage from '@react-native-async-storage/async-storage';
// Store auth token for reuse
const AUTH_TOKEN_KEY = 'solana_auth_token';
export const connectWallet = async (): Promise<string> => {
return new Promise(async (resolve, reject) => {
try {
const authorizationResult = await transact(async (wallet: Web3MobileWallet) => {
const authorizationResult = await wallet.authorize({
identity: APP_IDENTITY,
});
return authorizationResult;
});
// Store the auth token for future use
if (authorizationResult.auth_token) {
await AsyncStorage.setItem(AUTH_TOKEN_KEY, authorizationResult.auth_token);
}
// Use display_address directly (fallback to address if display_address not available)
const account = authorizationResult.accounts[0];
const address = (account as any).display_address || account.address;
resolve(address);
} catch (error) {
// ...
}
});
};
export const disconnectWallet = async (): Promise<void> => {
try {
// Clear stored auth token
await AsyncStorage.removeItem(AUTH_TOKEN_KEY);
} catch (error) {
console.error('Error clearing auth token:', error);
}
};
export const getStoredAuthToken = async (): Promise<string | null> => {
try {
return await AsyncStorage.getItem(AUTH_TOKEN_KEY);
} catch (error) {
console.error('Error getting stored auth token:', error);
return null;
}
};
Airdrop Service
airdropService.ts handles calling and confirming requestAirdrop for one SOL in lamports.
import { address, lamports } from '@solana/kit';
import { LAMPORTS_PER_SOL } from '../constants';
import type { RpcClient } from './rpcClient';
import { sleep } from '../utils/sleep';
export const requestAirdrop = async (
recipientAddress: string,
rpc: RpcClient
): Promise<string> => {
try {
console.log('Requesting airdrop for address:', recipientAddress);
// Convert address to @solana/kit address type
const solanaAddress = address(recipientAddress);
// Request airdrop using direct RPC call
const signature = await rpc.requestAirdrop(solanaAddress, lamports(BigInt(LAMPORTS_PER_SOL))).send();
console.log('Airdrop successful, signature:', signature);
// Wait for the transaction to be confirmed before returning
console.log('Waiting for transaction confirmation...');
// Poll for confirmation
let confirmed = false;
let attempts = 0;
const maxAttempts = 30; // 30 seconds max wait time
while (!confirmed && attempts < maxAttempts) {
await sleep(1000);
const { value: statuses } = await rpc.getSignatureStatuses([signature]).send();
if (statuses?.[0]?.confirmationStatus) {
confirmed = true;
console.log('Transaction confirmed!');
} else {
attempts++;
console.log(`Waiting for confirmation... attempt ${attempts}/${maxAttempts}`);
}
}
if (!confirmed) {
console.warn('Transaction confirmation timeout, but airdrop may still succeed');
}
return signature;
} catch (error) {
// ...
}
};
Transfer Service
transferService.ts handles sending SOL from one address to another. It does this by starting a MWA flow, building a transfer transaction with @solana/web3.js, and sending it with Kit's sendTransaction.
import { transact, Web3MobileWallet } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import { PublicKey, Transaction, SystemProgram, LAMPORTS_PER_SOL } from '@solana/web3.js';
import { fromByteArray } from 'base64-js';
import { APP_IDENTITY } from '../constants';
import { getStoredAuthToken } from './walletService';
import type { RpcClient } from './rpcClient';
import { sleep } from '../utils/sleep';
export const transferSol = async (
fromAddress: string,
toAddress: string,
amountSol: number,
rpc: RpcClient
): Promise<string> => {
try {
// Convert SOL amount to lamports
const amountLamports = Math.floor(amountSol * LAMPORTS_PER_SOL);
// Use mobile wallet adapter to sign and send the transaction
const signature = await transact(async (wallet: Web3MobileWallet) => {
// Add a small delay to ensure the UI is ready
await sleep(100);
// Try to reuse existing session with stored auth token
const storedAuthToken = await getStoredAuthToken();
if (storedAuthToken) {
try {
// Try silent reauthorization with stored token
await wallet.reauthorize({
auth_token: storedAuthToken,
identity: APP_IDENTITY,
});
} catch (reauthError) {
console.log('Silent reauth failed, falling back to full authorization');
// If silent reauth fails, fall back to full authorization
await wallet.authorize({
identity: APP_IDENTITY,
});
}
} else {
// No stored token, do full authorization
await wallet.authorize({
identity: APP_IDENTITY,
});
}
// Convert addresses to web3.js PublicKey for transaction building
const fromPubkey = new PublicKey(fromAddress);
const toPubkey = new PublicKey(toAddress);
// Create the transfer transaction using web3.js (required for mobile wallet adapter compatibility)
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey,
toPubkey,
lamports: amountLamports,
})
);
// Get recent blockhash using @solana/kit
const { value: blockhashResult } = await rpc.getLatestBlockhash().send();
transaction.recentBlockhash = blockhashResult.blockhash;
transaction.feePayer = fromPubkey;
// Sign the transaction using mobile wallet adapter
const signedTransactions = await wallet.signTransactions({
transactions: [transaction],
});
// Serialize the signed transaction to base64
const serializedTransaction = signedTransactions[0].serialize();
// Convert to Uint8Array (handles both Buffer and Uint8Array)
const txBytes = new Uint8Array(serializedTransaction);
const base64Transaction = fromByteArray(txBytes) as any;
// Send the signed transaction using @solana/kit
const txSignature = await rpc.sendTransaction(base64Transaction, { encoding: 'base64' }).send();
console.log('Transaction sent, signature:', txSignature);
return txSignature;
});
// Wait for the transaction to be confirmed before returning
console.log('Waiting for transaction confirmation...');
// Poll for confirmation (same pattern as airdrop)
// Note: signature from sendTransaction should already be compatible
let confirmed = false;
let attempts = 0;
const maxAttempts = 30; // 30 seconds max wait time
while (!confirmed && attempts < maxAttempts) {
await sleep(1000);
const { value: statuses } = await rpc.getSignatureStatuses([signature as any]).send();
if (statuses?.[0]?.confirmationStatus) {
confirmed = true;
console.log('Transaction confirmed!');
} else {
attempts++;
console.log(`Waiting for confirmation... attempt ${attempts}/${maxAttempts}`);
}
}
if (!confirmed) {
console.warn('Transaction confirmation timeout, but transfer may still succeed');
}
return String(signature);
} catch (error) {
// ...
}
};
Run the App
Make sure Android Studio is open and your emulator is running (first build may take a few minutes to complete).
npm run android
You should see the Home screen with a Connect Wallet button.

Connect Wallet
First, we need to connect to the Mock Wallet and enter your PIN to authorize.

After connecting, the Home screen shows Airdrop and Send SOL.

Request Airdrop
Tap Airdrop to fund your wallet for testing balance reads and transfers.
Devnet faucets are rate-limited. If throttled, use an alternate faucet. Check out A Complete Guide to Airdropping Test SOL on Solana for alternative ways to get devnet SOL.
Send SOL
Tap Send SOL, enter the recipient and amount, tap Send, then approve the transfer in Mock Wallet.

Approve the transfer with the Mock Wallet. On success, you’ll see a confirmation and an updated balance on the home screen.

Wrapping Up
Congrats! You now have the core foundation for building Solana mobile apps. You set your Android and React Native environment, integrated MWA, and built a working starter app that you can continue to extend.
Now that you understand the basics, you can extend this app to add more mobile-native features and publish to the Solana dApp Store.
Resources
- Solana Mobile App Example GitHub
- Android Studio Download Page
- React Native Docs
- Expo Docs
- Mock MWA Wallet Github
- Solana Mobile Developer Hub
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.