integration-privy について
Solana モバイルアプリで Privy のユーザー ID とサーバー検証可能なセッションを Mobile Wallet Adapter に連携するスキルです。ユーザーとセッションは Privy が管理し、鍵の保持と署名はウォレットアプリが担います。Android の開発ビルドが対象です。
主な機能
- 01SIWS によるログインセッション交換
- 02Privy ユーザーとウォレットの連携
- 03サーバー検証用アクセストークンの取得
- 04Expo のネイティブ依存関係と Provider の設定
- 05Privy とウォレットの両方からログアウト
活用シーン
- 01デバイス間で安定したユーザー ID を維持
- 02バックエンド要求に検証可能なセッションを提供
- 03ウォレット接続後に Privy ログインを追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ solana-mobile/ solana-mobile-skills --skill integration-privyインストール後、Claude Code で/integration-privyを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/setup.mdsiws.mdtroubleshooting.md詳細
Solana モバイルでの Privy
Privy が所有するのはユーザーです。これは永続的なアカウント識別子であり、バックエンドで検証できる JWT です。キーを所有するのは Mobile Wallet Adapter です。この構成では Privy は何も署名しません。署名はすべて、引き続きウォレットアプリから行われます。
Sign-In-With-Solana がこの2つを結び付けます。Privy がメッセージを生成し、MWA が署名し、Privy が署名をセッションと交換します。
デバイスをまたいで安定したユーザーレコードが必要な場合、サーバーで検証できるセッションが必要な場合、またはウォレット以外のログイン方法が必要な場合に利用してください。接続済みアドレスだけが必要なアプリに Privy は不要です。その場合は solana-mobile-wallet スキルだけを使用してください。
Android 専用で、開発ビルドでのみ動作します。 MWA は iOS をサポートしておらず、Expo Go では動作しないため、統合全体に制約があります。
開始する前に
| 要件 | 入手先 |
|---|---|
動作する useMobileWallet() | solana-mobile-wallet スキル |
| Android の開発ビルド | solana-mobile スキル |
| Privy のアプリ ID とクライアント ID | Privy ダッシュボード — ステップ 1 |
ステップ 1:Privy アプリを作成する
まずこれを行ってください。これらの値のうち2つはコンパイル時の環境変数であり、ダッシュボードのトグル1つでログインが機能するかどうかが決まります。
- https://dashboard.privy.io にサインインし、組織の概要画面で New app をクリックします
- 名前を付け、Mobile app を選択して作成し、App ID を保存します
- User management > Authentication の External wallets カードで、SVM (Solana) wallets を有効にします
- App settings > Clients で、アプリ識別子に
app.jsonのexpo.android.packageの値を設定し、Client ID を保存します
SVM wallets のトグルは見落としやすく、デバッグに手間がかかる項目です。オフの間は、ウォレットが正しく署名していても、login はすべての SIWS の試行を拒否します。アプリ識別子が重要なのは、Privy が呼び出し元アプリのパッケージ名をクライアントと照合するためです。
EXPO_PUBLIC_PRIVY_APP_ID=your-privy-app-id
EXPO_PUBLIC_PRIVY_CLIENT_ID=your-privy-client-id
どちらも公開クライアント側の識別子なので、EXPO_PUBLIC_ が適切です。Privy のアプリシークレットをモバイルアプリに含めてはいけません。EXPO_PUBLIC_ が付いたものは、すべて配布バンドルから読み取れます。シークレットはサーバーコード専用です。
ステップ 2:インストールして設定する
npx expo install @privy-io/expo @privy-io/expo-native-extensions
@privy-io/expo は多数のピア依存関係を持ち、その内容はリリースごとに変わります。パスキー、セキュアストア、ウェブブラウザー、暗号処理、viem などです。どこかの一覧をそのままコピーするのではなく、ここにあるものも含め、選んだバージョンが要求するものをインストールしてください。
ネイティブ側の設定は3つ必要で、SDK はそれぞれ異なる箇所で失敗します。
- エントリーモジュールで他の処理より先に読み込む、暗号処理とテキストエンコーディングのポリフィル
app.jsonのプラグインに設定するexpo-secure-storeとexpo-web-browserjoseをブラウザー向けビルドに解決するための Metro リゾルバーのオーバーライド
それぞれの完全な内容と、適用を確認する方法はこちらです:references/setup.md。
このステップの後にネイティブで再ビルドしてください(npx expo run:android)。JS の再読み込みでは、新しいネイティブモジュールは反映されません。
ステップ 3:プロバイダーをマウントする
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>
)
}
PrivyProvider と MobileWalletProvider は互いに依存しないため、どちらを内側にネストしても構いません。ただし、両方ともすべての画面より上に配置する必要があります。また、以下のフックがクエリとミューテーションの場合は、QueryClientProvider を両方より上に配置してください。
環境変数がない場合に例外をスローするのは意図的です。未定義の値を Privy に渡すと、不正なアプリ ID として扱われ、かなり後になって原因の分かりにくい初期化エラーとして表面化します。
clientId は PrivyProviderProps では任意として型付けされていますが、ここでは誤解を招きます。Privy のモバイル向けドキュメントでは必須として扱われ、ダッシュボードではモバイルクライアントごとに発行されます。必ず渡してください。
Step 4: isReady を待つ
usePrivy() が返す状態は、SDK が保存済みトークンの読み取りを完了するまで意味を持ちません。
| 値 | 型 | 備考 |
|---|---|---|
isReady | boolean | これが true になるまでは、ほかの値はすべて暫定的です |
user | User | null | 未認証の場合は null です。undefined にはなりません |
error | Error | null | 初期化エラーです。通常はストレージへのアクセスが原因です |
logout | () => Promise<void> | サインイン中のユーザーがいない場合は何もしません |
getAccessToken | () => Promise<string | null> | リクエストごとに呼び出してください。結果をキャッシュしないでください |
const { error, isReady, user } = usePrivy()
if (!isReady) return <Loading />
if (error) return <ErrorCard message={error.message} />
isReady が false の間にサインアウト状態を表示すると、すでに認証済みのユーザーが起動のたびにログイン画面を一瞬見ることになります。
Step 5: SIWS でサインインする
連携の流れは「生成、署名、交換」の 1 つの手順です。
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) })
},
})
}
ウォレット接続後にのみ呼び出してください。そうしないと signMessages が独自に認証を要求するため、useMobileWallet().account が定義済みである必要があります。
この処理が機能するかどうかは、次の 3 つのエンコードの詳細で決まります。
- base58 の
account.addressを渡してください。account.addressBase64もありますが、これは MWA のワイヤ形式であり、Privy は受け付けません。Privy のレシピでは、ロープロトコルを扱うために base64 を base58 に変換する処理に 3 行を費やしていますが、@wallet-ui/react-native-kitはすでにその変換を行っています。 fromUint8Arrayが生成するのは base58 ではなく base64 です。 これはjs-base64の再エクスポートです。ここでは Privy が base64 文字列を必要とします。base58 では検証に失敗します。- バイト列を切り詰めないでください。
signMessagesが解決する値は、MWA の 署名済みペイロード であり、単独の 64 バイト署名ではありません。全体を base64 にエンコードして渡してください。テンプレートと Privy のレシピは、どちらもまさにこの処理を行います。
from.domain は RFC 3986 の authority です。スキームやパスを含まない、ホスト名のみを指定します。from.uri は完全な URI で、通常はアプリのディープリンクです。どちらも一貫性を保ってください。これらは、ウォレット上でユーザーに表示される署名対象メッセージに埋め込まれます。
既存アカウントへのウォレットのリンク、およびサーバーでのセッション検証については、references/siws.md を参照してください。
Step 6: 両方からサインアウトする
const { logout } = usePrivy()
const { disconnect } = useMobileWallet()
await logout()
await disconnect()
どちらか一方だけを行うと、アプリは中途半端なサインアウト状態になります。disconnect() だけではウォレットが切断されたまま Privy のセッションが有効な状態になり、logout() だけではウォレットの認証状態が残るため、次回の試行時にサイレントで再度サインインします。
それぞれが担当するもの
| 項目 | 担当 |
|---|---|
| 秘密鍵と署名 | MWA 経由のウォレットアプリ |
| 接続済みアドレス | useMobileWallet().account |
| デバイス間でのユーザー ID | usePrivy().user |
| サーバーで検証可能なセッション | usePrivy().getAccessToken() |
| トランザクションの送信 | useMobileWallet().sendTransactions |
この構成には Privy の署名機能はありません。Privy にサインインしていることと、MWA 経由で接続していることは、独立した 2 つの状態です。UI ではあらゆる組み合わせに対応する必要があります。特に重要なのは「接続済みだがサインインしていない」状態で、この場合にサインインボタンを表示します。
参考資料
- references/setup.md — ポリフィル、Metro の設定、
app.jsonプラグイン、環境変数、およびそれぞれが反映されたことの確認方法 - references/siws.md — SIWS による交換の詳しい説明、追加ウォレットのリンク、サーバーサイドでのトークン検証、Wallet UI を使わないロープロトコル版
- references/troubleshooting.md — Privy 固有のエラーとその原因
ここで紹介するパターンは、完全に動作するアプリである
expo-kit-privy
に基づいています。このファイルの内容が不明確な場合は、そちらを参照してください。
npx solana-mobile@latest create /tmp/reference-app --template expo-kit-privy --skip-install
関連スキル
solana-mobile-wallet— このスキルの基盤となる MWA の接続、署名、送信solana-mobile— 開発ビルド、エミュレーター、ツールチェーンの確認seeker-genesis-token— JWT を使うほどではない場合に、Privy を使わずサーバーサイドで SIWS を検証する方法
リンク
- 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