integration-privy
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePrivy on Solana mobile
在Solana移动应用中集成Privy
Privy owns the user: a durable account identifier and a JWT a backend can verify. Mobile
Wallet Adapter owns the keys. Privy signs nothing in this setup — every signature still
comes from the wallet app.
Sign-In-With-Solana joins the two. Privy generates a message, MWA signs it, Privy exchanges
the signature for a session.
Reach for this when an app needs a stable user record across devices, a server-verifiable
session, or login methods beyond a wallet. An app that only needs a connected address does not
need Privy — use the skill alone.
solana-mobile-walletAndroid only, and a development build only. MWA has no iOS support and does not run in
Expo Go, which caps the whole integration.
Privy负责用户管理:提供持久化的账户标识和后端可验证的JWT。Mobile
Wallet Adapter负责密钥管理。在此配置中,Privy不进行任何签名操作——所有签名仍由钱包应用生成。
Sign-In-With-Solana将二者关联起来:Privy生成消息,MWA对其签名,Privy再用签名换取会话。
当应用需要跨设备的稳定用户记录、服务器可验证的会话,或钱包之外的登录方式时,可采用此方案。如果应用仅需连接地址,则无需Privy——单独使用技能即可。
solana-mobile-wallet仅支持安卓平台,且仅适用于开发构建版本。 MWA暂不支持iOS,也无法在Expo Go中运行,这限制了整个集成方案的适用范围。
Before you start
准备工作
| Requirement | Where it comes from |
|---|---|
A working | |
| A development build on Android | |
| A Privy app ID and client ID | The Privy dashboard — step 1 |
| 要求 | 来源 |
|---|---|
可用的 | |
| 安卓平台的开发构建包 | |
| Privy应用ID和客户端ID | Privy控制台——步骤1 |
Step 1: create the Privy app
步骤1:创建Privy应用
Do this first. Two of these values are compile-time environment variables, and one dashboard
toggle decides whether login works at all.
- Sign in at https://dashboard.privy.io and click New app on the organization overview
- Name it, select Mobile app, create it, and save the App ID
- Under User management > Authentication, in the External wallets card, enable SVM (Solana) wallets
- Under App settings > Clients, set the app identifier to the value from
expo.android.package, and save the Client IDapp.json
The SVM wallets toggle is the one that is easy to skip and expensive to debug — while it is
off, rejects every SIWS attempt even though the wallet signed correctly. The app
identifier matters because Privy checks the calling app's package name against the client.
loginbash
EXPO_PUBLIC_PRIVY_APP_ID=your-privy-app-id
EXPO_PUBLIC_PRIVY_CLIENT_ID=your-privy-client-idBoth are public client-side identifiers, so is correct. The Privy app secret
never belongs in a mobile app — anything prefixed is readable in the shipped
bundle. The secret is for server code only.
EXPO_PUBLIC_EXPO_PUBLIC_请先完成此步骤。其中两个值是编译时环境变量,还有一个控制台开关直接决定登录功能是否可用。
- 登录https://dashboard.privy.io,在组织概览页面点击**New app**
- 为应用命名,选择Mobile app,创建应用并保存App ID
- 在User management > Authentication下的External wallets卡片中,启用SVM (Solana) wallets
- 在App settings > Clients中,将应用标识符设置为中的
app.json值,并保存Client IDexpo.android.package
SVM wallets开关很容易被忽略,且调试成本很高——如果关闭该开关,即使钱包签名正确,也会拒绝所有SIWS请求。应用标识符至关重要,因为Privy会验证调用应用的包名是否与客户端匹配。
loginbash
EXPO_PUBLIC_PRIVY_APP_ID=your-privy-app-id
EXPO_PUBLIC_PRIVY_CLIENT_ID=your-privy-client-id这两个都是公开的客户端标识符,因此使用前缀是正确的。Privy应用密钥绝不能放入移动应用中——任何以为前缀的内容在打包后的应用中都是可读的。密钥仅适用于服务器代码。
EXPO_PUBLIC_EXPO_PUBLIC_Step 2: install and configure
步骤2:安装与配置
bash
npx expo install @privy-io/expo @privy-io/expo-native-extensions@privy-io/expoviemThree pieces of native wiring are required, and the SDK fails in a different place for each:
- Crypto and text-encoding polyfills, loaded from the entry module before anything else
- and
expo-secure-storeinexpo-web-browserpluginsapp.json - A Metro resolver override so resolves to its browser build
jose
Full contents for each, and how to confirm they took: references/setup.md.
Rebuild natively () after this step — a JS reload will not pick up the
new native modules.
npx expo run:androidbash
npx expo install @privy-io/expo @privy-io/expo-native-extensions@privy-io/expoviem需要完成三项原生配置,每项配置缺失都会导致SDK在不同环节失败:
- 加密和文本编码polyfill,需在入口模块加载所有内容前导入
- 在的plugins中添加
app.json和expo-secure-storeexpo-web-browser - 覆盖Metro解析器,使解析为其浏览器构建版本
jose
各项配置的完整内容及验证方式:references/setup.md。完成此步骤后请重新构建原生应用()——仅重载JS无法加载新的原生模块。
npx expo run:androidStep 3: mount the providers
步骤3:挂载提供者组件
tsx
import { PrivyProvider } from '@privy-io/expo'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { type AppIdentity, createSolanaDevnet, MobileWalletProvider } from '@wallet-ui/react-native-kit'
import type { ReactNode } from 'react'
const cluster = createSolanaDevnet()
const identity: AppIdentity = { name: 'My App', uri: 'myapp://myapp' }
const privyAppId = process.env.EXPO_PUBLIC_PRIVY_APP_ID
const privyClientId = process.env.EXPO_PUBLIC_PRIVY_CLIENT_ID
const queryClient = new QueryClient()
export function AppProviders({ children }: { children: ReactNode }) {
if (!privyAppId || !privyClientId) {
throw new Error('Missing Privy environment variables')
}
return (
<QueryClientProvider client={queryClient}>
<PrivyProvider appId={privyAppId} clientId={privyClientId}>
<MobileWalletProvider cluster={cluster} identity={identity}>
{children}
</MobileWalletProvider>
</PrivyProvider>
</QueryClientProvider>
)
}PrivyProviderMobileWalletProviderQueryClientProviderThrowing on missing environment variables is deliberate. Undefined values reach Privy as a
malformed app ID and surface much later as an opaque initialization error.
clientIdPrivyProviderPropstsx
import { PrivyProvider } from '@privy-io/expo'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { type AppIdentity, createSolanaDevnet, MobileWalletProvider } from '@wallet-ui/react-native-kit'
import type { ReactNode } from 'react'
const cluster = createSolanaDevnet()
const identity: AppIdentity = { name: 'My App', uri: 'myapp://myapp' }
const privyAppId = process.env.EXPO_PUBLIC_PRIVY_APP_ID
const privyClientId = process.env.EXPO_PUBLIC_PRIVY_CLIENT_ID
const queryClient = new QueryClient()
export function AppProviders({ children }: { children: ReactNode }) {
if (!privyAppId || !privyClientId) {
throw new Error('Missing Privy environment variables')
}
return (
<QueryClientProvider client={queryClient}>
<PrivyProvider appId={privyAppId} clientId={privyClientId}>
<MobileWalletProvider cluster={cluster} identity={identity}>
{children}
</MobileWalletProvider>
</PrivyProvider>
</QueryClientProvider>
)
}PrivyProviderMobileWalletProviderQueryClientProvider故意在缺失环境变量时抛出错误是合理的。未定义的值会被Privy视为格式错误的应用ID,并在后续阶段表现为模糊的初始化错误。
clientIdPrivyProviderPropsclientIdStep 4: wait for isReady
isReady步骤4:等待isReady
状态
isReadyusePrivy()| Value | Type | Notes |
|---|---|---|
| | Everything else is provisional until this is |
| | |
| | Initialization failures, typically storage access |
| | No-op when nobody is signed in |
| | Call per request; never cache the result |
tsx
const { error, isReady, user } = usePrivy()
if (!isReady) return <Loading />
if (error) return <ErrorCard message={error.message} />Rendering a signed-out state while is makes an already-authenticated user
flash through a login screen on every cold start.
isReadyfalseusePrivy()| 值 | 类型 | 说明 |
|---|---|---|
| | 在此值变为 |
| | 未认证时为 |
| | 初始化失败,通常是存储访问问题 |
| | 未登录时无任何操作 |
| | 每次请求时调用;切勿缓存结果 |
tsx
const { error, isReady, user } = usePrivy()
if (!isReady) return <Loading />
if (error) return <ErrorCard message={error.message} />如果在为时渲染未登录状态,会导致已认证用户在每次冷启动时短暂闪过登录界面。
isReadyfalseStep 5: sign in with SIWS
步骤5:通过SIWS登录
The whole integration is this one sequence: generate, sign, exchange.
tsx
import { useLoginWithSiws } from '@privy-io/expo'
import type { Address } from '@solana/kit'
import { useMutation } from '@tanstack/react-query'
import { fromUint8Array, useMobileWallet } from '@wallet-ui/react-native-kit'
const siwsDomain = 'myapp.com'
const siwsUri = 'myapp://privy-login'
export function usePrivySignInMutation(address: Address) {
const { generateMessage, login } = useLoginWithSiws()
const { signMessages } = useMobileWallet()
return useMutation({
mutationFn: async () => {
const { message } = await generateMessage({
from: { domain: siwsDomain, uri: siwsUri },
wallet: { address: address.toString() },
})
const signedPayload = await signMessages(new TextEncoder().encode(message))
await login({ message, signature: fromUint8Array(signedPayload) })
},
})
}Call it only once a wallet is connected — must be defined, since
triggers its own authorization otherwise.
useMobileWallet().accountsignMessagesThree encoding details decide whether this works:
- Pass , which is base58.
account.addressalso exists; it is MWA's wire format and Privy will not accept it. Privy's own recipe spends three lines converting base64 to base58 because it drives the raw protocol —account.addressBase64has already done that conversion for you.@wallet-ui/react-native-kit - produces base64, not base58. It is a re-export of
fromUint8Array. Privy wants the base64 string here; base58 fails verification.js-base64 - Do not slice the bytes. resolves to MWA's signed payload, not a bare 64-byte signature. Base64-encode it whole and hand it over — the template and Privy's recipe both do exactly this.
signMessages
from.domainfrom.uriLinking a wallet to an account that already exists, and verifying the session on a server:
references/siws.md.
整个集成流程就是这三个步骤:生成消息、签名、换取会话。
tsx
import { useLoginWithSiws } from '@privy-io/expo'
import type { Address } from '@solana/kit'
import { useMutation } from '@tanstack/react-query'
import { fromUint8Array, useMobileWallet } from '@wallet-ui/react-native-kit'
const siwsDomain = 'myapp.com'
const siwsUri = 'myapp://privy-login'
export function usePrivySignInMutation(address: Address) {
const { generateMessage, login } = useLoginWithSiws()
const { signMessages } = useMobileWallet()
return useMutation({
mutationFn: async () => {
const { message } = await generateMessage({
from: { domain: siwsDomain, uri: siwsUri },
wallet: { address: address.toString() },
})
const signedPayload = await signMessages(new TextEncoder().encode(message))
await login({ message, signature: fromUint8Array(signedPayload) })
},
})
}仅在钱包连接后调用此函数——必须已定义,否则会触发自身的授权流程。
useMobileWallet().accountsignMessages三个编码细节决定了此流程是否能正常工作:
- 传入,它是base58格式。 还存在
account.address格式;这是MWA的有线格式,Privy不会接受它。Privy官方示例需要用三行代码将base64转换为base58,因为它基于原始协议——而account.addressBase64已经为你完成了这个转换。@wallet-ui/react-native-kit - 生成base64格式,而非base58。 它是
fromUint8Array的重导出。Privy在此处需要base64字符串;base58会验证失败。js-base64 - 不要截取字节。 返回的是MWA的签名载荷,而非裸64字节签名。将其完整进行base64编码后传入——示例模板和Privy官方示例都是这么做的。
signMessages
from.domainfrom.uri将钱包关联至现有账户,以及在服务器端验证会话:references/siws.md。
Step 6: sign out of both
步骤6:同时退出两者
tsx
const { logout } = usePrivy()
const { disconnect } = useMobileWallet()
await logout()
await disconnect()Doing one without the other leaves the app in a half-signed-out state. alone
keeps a live Privy session with no wallet behind it; alone leaves the wallet
authorized and re-signs in silently on the next attempt.
disconnect()logout()tsx
const { logout } = usePrivy()
const { disconnect } = useMobileWallet()
await logout()
await disconnect()只执行其中一个操作会导致应用处于半退出状态。仅调用会保留有效的Privy会话,但背后没有钱包;仅调用会保留钱包授权,下次尝试时会自动静默登录。
disconnect()logout()Which side owns what
职责划分
| Concern | Owner |
|---|---|
| Private keys and signing | The wallet app, over MWA |
| Connected address | |
| User identity across devices | |
| Server-verifiable session | |
| Sending transactions | |
There is no Privy signer in this setup. A user is signed in to Privy and connected over MWA as
two independent facts, and the UI has to handle every combination — most usefully "connected
but not signed in", which is where the sign-in button belongs.
| 事项 | 负责方 |
|---|---|
| 私钥与签名 | 钱包应用(通过MWA) |
| 连接地址 | |
| 跨设备用户标识 | |
| 服务器可验证会话 | |
| 发送交易 | |
此配置中没有Privy签名器。用户登录Privy和通过MWA连接是两个独立的状态,UI需要处理所有组合情况——最常见的是"已连接但未登录",这正是登录按钮应出现的场景。
Reference material
参考资料
- references/setup.md — polyfills, Metro config, plugins, environment variables, and how to verify each one landed
app.json - references/siws.md — the SIWS exchange in depth, linking additional wallets, server-side token verification, and the raw-protocol variant without Wallet UI
- references/troubleshooting.md — Privy-specific failures and their causes
The patterns here follow
,
a complete working app. Read it when this file is ambiguous:
expo-kit-privybash
npx solana-mobile@latest create /tmp/reference-app --template expo-kit-privy --skip-install- references/setup.md —— polyfill、Metro配置、插件、环境变量,以及如何验证每项配置是否生效
app.json - references/siws.md —— 深入讲解SIWS交互流程、关联额外钱包、服务器端令牌验证,以及不使用Wallet UI的原始协议变体
- references/troubleshooting.md —— Privy相关故障及其原因
此处的实现模式遵循,一个完整的可运行应用。当本文档内容不明确时,可参考该应用:
expo-kit-privybash
npx solana-mobile@latest create /tmp/reference-app --template expo-kit-privy --skip-installRelated skills
相关技能
- — MWA connection, signing, and sending, which this builds on
solana-mobile-wallet - — development builds, emulators, toolchain checks
solana-mobile - — SIWS verified server-side without Privy, when a JWT is overkill
seeker-genesis-token
- —— MWA连接、签名和交易发送,是本方案的基础
solana-mobile-wallet - —— 开发构建、模拟器、工具链检查
solana-mobile - —— 不使用Privy的SIWS服务器端验证,适用于无需JWT的场景
seeker-genesis-token
Links
链接
- Privy Solana MWA recipe: https://docs.privy.io/recipes/solana/adding-solana-mwa
- Privy Expo SIWS login: https://docs.privy.io/guide/expo/authentication/siws
- Privy dashboard: https://dashboard.privy.io
- Privy Solana MWA官方指南:https://docs.privy.io/recipes/solana/adding-solana-mwa
- Privy Expo SIWS登录指南:https://docs.privy.io/guide/expo/authentication/siws
- Privy控制台:https://dashboard.privy.io