--- name: solana-mobile-wallet description: Connect Solana wallets and sign or send transactions in React Native Expo apps using Mobile Wallet Adapter and Wallet UI. Use when adding a connect wallet button, showing a connected address, disconnecting, signing messages, sign-in with Solana, transferring SOL, or sending any transaction from a Solana mobile app. --- # Solana wallets on mobile Wallet connection and transaction signing through Mobile Wallet Adapter (MWA), wrapped by `@wallet-ui/react-native-kit` (or `@wallet-ui/react-native-web3js` on the legacy stack). **MWA requires a development build on Android. Expo Go will not work.** If the project has no development build yet, or does not exist, start with the `solana-mobile` skill. ## Step 1: pick the stack — do this before writing any code **Write kit code.** `@solana/kit` with `@wallet-ui/react-native-kit` is the stack to reach for, and everything in this file describes it. There is exactly one reason to write `@solana/web3.js` instead: the project already runs on it. Check `package.json` first. | `package.json` says | Do this | | --- | --- | | `@wallet-ui/react-native-kit`, or no Solana client yet | Kit. This file, plus [references/kit.md](references/kit.md) | | `@wallet-ui/react-native-web3js` is the app's Solana client | [references/web3js.md](references/web3js.md) | | The user explicitly asked for web3.js | [references/web3js.md](references/web3js.md), and say why kit would be better | Do not introduce web3.js into a kit project, or mix the two in one app. Their provider props, hook return values, and transaction construction all differ, so code from one silently fails on the other. If a project has no Solana client at all, that is a new build — use kit. Adding a *new* wallet feature to an existing web3.js app is not a reason to migrate mid-task. Match what is there, and mention migration as a follow-up if it seems worth it. ## Step 2: confirm the provider is mounted `useMobileWallet` returns empty state without `MobileWalletProvider` above it. Look for it in the root layout or an app-providers module. Build the cluster with the `createSolana*` helpers rather than by hand — a `SolanaCluster` also needs a `label`, which the helpers fill in: ```tsx import { type AppIdentity, createSolanaDevnet, MobileWalletProvider, type SolanaCluster, } from '@wallet-ui/react-native-kit' const identity: AppIdentity = { name: 'My App' } const cluster: SolanaCluster = createSolanaDevnet({ url: 'https://api.devnet.solana.com' }) {children} ``` `createSolanaDevnet`, `createSolanaTestnet`, and `createSolanaLocalnet` take optional props; `createSolanaMainnet` requires a `url`, since there is no sensible public default for mainnet. The provider props are `cluster`, `identity`, and optional `cache`, `createClient`, `children`. There is **no `chain` prop and no `endpoint` prop** — passing those does nothing. Every `AppIdentity` field is optional. `name` alone is enough to get started; add `uri` as a real deep link for anything shipping, since wallets display it during authorization and a placeholder can read as a phishing attempt. Put `QueryClientProvider` from `@tanstack/react-query` above the wallet provider — the hook patterns below are queries and mutations. ## Step 3: use the hook ```tsx import { useMobileWallet } from '@wallet-ui/react-native-kit' const { account, connect, disconnect, client } = useMobileWallet() ``` What the hook actually returns on the kit stack: | Value | Type | Notes | | --- | --- | --- | | `account` | `Account \| undefined` | `undefined` when disconnected, not `null` | | `accounts` | `Account[] \| null` | All authorized accounts | | `connect` | `() => Promise` | Opens the wallet picker | | `disconnect` | `() => Promise` | | | `client` | `Client` | Kit client — use `client.rpc` for RPC calls | | `chain` | `SolanaClusterId` | The active cluster. Put it in query keys | | `sendTransactions` | `(instructions: Instruction[]) => Promise` | Simplest send path | | `signAndSendTransaction` | `(tx, minContextSlot) => Promise` | Note the second argument | | `signTransaction` | `(tx) => Promise` | Sign without broadcasting | | `signMessages` | `(msg: Uint8Array) => Promise` | | | `signIn` | `(payload) => Promise` | Sign-in with Solana; can also connect | | `identity`, `store` | | Config and authorization store | Singular aliases exist for several of these — `sendTransaction`, `signMessage`, `signTransactions` — with the same signatures. The templates use the plural forms; either works, so follow whatever the project already uses. Four things that trip people up: 1. **There is no `connected` boolean.** Derive it: `const connected = !!account`. 2. **`signAndSendTransaction` takes `minContextSlot` as a second argument.** Calling it with only a transaction fails. Get the slot from `getLatestBlockhash`, or use `sendTransactions(instructions)`, which handles this for you. 3. **The kit hook exposes `client`, not `connection`.** `connection` only exists on the web3.js stack. 4. **Include `chain` in every React Query key that holds chain data.** Otherwise switching cluster serves the previous network's cached balances, which looks like a wallet bug. `account.address` is a kit `Address` (a branded string), so it interpolates into text directly. `account.label` is the wallet-supplied name and may be undefined. ## Connect and disconnect ```tsx import { Pressable, Text } from 'react-native' import { useMobileWallet } from '@wallet-ui/react-native-kit' export function ConnectButton() { const { account, connect, disconnect } = useMobileWallet() async function onPress() { try { if (account) await disconnect() else await connect() } catch (error) { // The user dismissing the wallet picker lands here. Do not treat it as a crash. console.error(error) } } return ( {account ? 'Disconnect' : 'Connect Wallet'} ) } ``` Always wrap `connect()` in try/catch — cancelling the wallet picker rejects the promise. Cancellation is a normal outcome, not an error state worth alarming the user about; see [references/kit.md](references/kit.md) for how to tell cancellation apart from real failures. Authorization is cached, so the app reconnects on restart without a new prompt. ## Read chain data Use `client` from the hook. There is no need to build a client of your own: ```tsx import type { Address } from '@solana/kit' import { useQuery } from '@tanstack/react-query' import { useMobileWallet } from '@wallet-ui/react-native-kit' export function useGetBalance({ address }: { address: Address }) { const { chain, client } = useMobileWallet() return useQuery({ queryFn: () => client.rpc.getBalance(address).send(), queryKey: ['get-balance', chain, address], }) } ``` Kit RPC calls are lazy: `client.rpc.someMethod(...)` builds a request and `.send()` runs it. Forget `.send()` and nothing errors — the data simply never arrives. Balances come back as `bigint` lamports. Convert deliberately, and never with `parseFloat`: ```ts export function lamportsToSol(lamports: bigint) { return Number(lamports) / 1e9 } ``` ## Send a transaction Build instructions and hand them over. This covers most cases: ```tsx import { getAddMemoInstruction } from '@solana-program/memo' import type { Instruction } from '@solana/kit' const { sendTransactions } = useMobileWallet() const instructions: Instruction[] = [getAddMemoInstruction({ memo: 'gm' })] const signature = await sendTransactions(instructions) ``` `sendTransactions` handles blockhash, `minContextSlot`, fee payer, and signature decoding. Reach for the explicit `pipe` form only when you need fee-payer control, a specific blockhash lifetime, or a fee pre-check. Full worked example, with the balance-versus-fee assertion and signature decoding: [references/kit.md](references/kit.md). ## Reference material - [references/kit.md](references/kit.md) — kit stack: clusters and config, reading chain data, transactions, sign-in with Solana, message signing, error handling - [references/web3js.md](references/web3js.md) — legacy `@solana/web3.js` stack, and a migration sketch - [references/troubleshooting.md](references/troubleshooting.md) — connection and signing failures with known causes When something here is ambiguous, read the template. The patterns in this skill follow [`expo-kit-minimal`](https://github.com/solana-mobile/templates/tree/main/mobile/expo-kit-minimal), which is a complete working app and stays current in a way prose does not: ```bash npx solana-mobile@latest create /tmp/reference-app --template expo-kit-minimal --skip-install ``` ## Related skills - `solana-mobile` — project setup, templates, emulators, development builds - `integration-privy` — add Privy accounts and sessions on top of this wallet connection - `seeker-genesis-token` — verify Seeker device ownership after connecting - `seeker-domains` — display `.skr` names instead of raw addresses ## Links - Wallet UI: https://wallet-ui.dev - MWA docs: https://docs.solanamobile.com/react-native/overview - Solana Kit: https://www.solanakit.com