К основному содержанию
скилл CX Codex

integration-privy — скилл для Claude Code: Privy в Solana Expo

Скилл для подключения Privy-аутентификации к Solana Expo Android-приложению поверх Mobile Wallet Adapter через Sign-In-With-Solana.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add solana-mobile/solana-mobile-skills --skill integration-privy -a claude-code
Открыть на GitHub

Что делает integration-privy

Скилл разводит две роли. Privy владеет пользователем: постоянный идентификатор аккаунта и JWT, который бэкенд может проверить. Mobile Wallet Adapter владеет ключами, и подпись по-прежнему приходит из кошелька. Sign-In-With-Solana связывает их: Privy генерирует сообщение, MWA его подписывает, Privy обменивает подпись на сессию.

Внутри — пошаговый разбор от создания приложения в дашборде Privy до входа через useLoginWithSiws. Нужны рабочий useMobileWallet(), development build на Android и пара идентификаторов из дашборда: App ID и Client ID в EXPO_PUBLIC_PRIVY_APP_ID и EXPO_PUBLIC_PRIVY_CLIENT_ID. В дашборде придётся включить SVM-кошельки и указать app identifier, совпадающий с expo.android.package из app.json. Тумблер SVM легко пропустить, и потом это дорого отлаживать: пока он выключен, login отклоняет любую попытку SIWS, хотя кошелёк подписал корректно.

Ограничение жёсткое: только Android и только development build. MWA не поддерживает iOS и не работает в Expo Go, поэтому вся интеграция упирается в это. Секрет приложения Privy в мобильное приложение не кладут — всё с префиксом EXPO_PUBLIC_ читается из собранного бандла, секрет остаётся серверному коду. Если приложению нужен только подключённый адрес, Privy не требуется.

Какие задачи решает

  • добавить Privy-аутентификацию в Solana Expo Android-приложение
  • войти в аккаунт через Sign-In-With-Solana
  • привязать кошелёк к существующему аккаунту Privy
  • прочитать access token Privy на бэкенде
  • разобраться, почему Privy не работает вместе с MWA

Примеры запросов агенту

  • Подключи Privy к моему Solana Expo Android-приложению поверх Mobile Wallet Adapter
  • Настрой вход через useLoginWithSiws и привяжи кошелёк к аккаунту Privy
  • Помоги отладить связку Privy и MWA: login отклоняется после подписи

Как установить integration-privy

1Через Skills CLI (Vercel)

npx skills add solana-mobile/solana-mobile-skills --skill integration-privy -a claude-code

2Вручную: положить файлы

~/.claude/skills/integration-privy/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/integration-privy/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Вопросы о integration-privy

Как установить integration-privy в Claude?

Выполните npx skills add solana-mobile/solana-mobile-skills --skill integration-privy -a claude-code. Скилл появится в ~/.claude/skills/integration-privy/SKILL.md.

Что делает integration-privy?

Проводит по подключению Privy к Solana Expo Android-приложению: создание приложения в дашборде, установка @privy-io/expo, монтирование провайдеров, ожидание isReady и вход через useLoginWithSiws.

Почему login отклоняется, хотя кошелёк подписал сообщение?

Скорее всего в дашборде Privy не включены SVM-кошельки. Пока этот тумблер выключен, login отклоняет каждую попытку SIWS, даже если подпись верная.

Исходный текст

SKILL.md · Apache-2.0
---
name: integration-privy
description: Add Privy authentication to a Solana Expo Android app on top of Mobile Wallet Adapter, using Sign-In-With-Solana. Use when installing @privy-io/expo, mounting PrivyProvider, logging a user in with useLoginWithSiws, linking a wallet to an existing Privy account, reading the Privy access token from a backend, or debugging a Privy plus MWA setup.
---

# Privy on Solana mobile

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 `solana-mobile-wallet` skill alone.

**Android only, and a development build only.** MWA has no iOS support and does not run in
Expo Go, which caps the whole integration.

## Before you start

| Requirement | Where it comes from |
| --- | --- |
| A working `useMobileWallet()` | `solana-mobile-wallet` skill |
| A development build on Android | `solana-mobile` skill |
| A Privy app ID and client ID | The Privy dashboard — step 1 |

## Step 1: create the Privy app

Do this first. Two of these values are compile-time environment variables, and one dashboard
toggle decides whether login works at all.

1. Sign in at https://dashboard.privy.io and click **New app** on the organization overview
2. Name it, select **Mobile app**, create it, and save the **App ID**
3. Under **User management > Authentication**, in the **External wallets** card, enable
   **SVM (Solana) wallets**
4. Under **App settings > Clients**, set the app identifier to the `expo.android.package`
   value from `app.json`, and save the **Client ID**

The **SVM wallets** toggle is the one that is easy to skip and expensive to debug — while it is
off, `login` 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.

```bash
EXPO_PUBLIC_PRIVY_APP_ID=your-privy-app-id
EXPO_PUBLIC_PRIVY_CLIENT_ID=your-privy-client-id
```

Both are public client-side identifiers, so `EXPO_PUBLIC_` is correct. **The Privy app secret
never belongs in a mobile app** — anything prefixed `EXPO_PUBLIC_` is readable in the shipped
bundle. The secret is for server code only.

## Step 2: install and configure

```bash
npx expo install @privy-io/expo @privy-io/expo-native-extensions
```

`@privy-io/expo` carries a long peer dependency list that shifts between releases — passkeys,
secure store, web browser, crypto, `viem`. Install what the version you picked asks for rather
than copying a list from anywhere, including from here.

Three 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
- `expo-secure-store` and `expo-web-browser` in `app.json` plugins
- A Metro resolver override so `jose` resolves to its browser build

Full contents for each, and how to confirm they took: [references/setup.md](references/setup.md).
Rebuild natively (`npx expo run:android`) after this step — a JS reload will not pick up the
new native modules.

## Step 3: mount the providers

```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>
  )
}
```

`PrivyProvider` and `MobileWalletProvider` do not depend on each other, so their relative
nesting is free — but both must sit above every screen, and `QueryClientProvider` above both
if the hooks below are queries and mutations.

Throwing on missing environment variables is deliberate. Undefined values reach Privy as a
malformed app ID and surface much later as an opaque initialization error.

`clientId` is typed optional in `PrivyProviderProps`, which is misleading here: Privy's mobile
documentation treats it as required, and the dashboard issues one per mobile client. Pass it.

## Step 4: wait for `isReady`

`usePrivy()` returns state that is meaningless until the SDK finishes reading stored tokens:

| Value | Type | Notes |
| --- | --- | --- |
| `isReady` | `boolean` | Everything else is provisional until this is `true` |
| `user` | `User \| null` | `null` when unauthenticated — not `undefined` |
| `error` | `Error \| null` | Initialization failures, typically storage access |
| `logout` | `() => Promise<void>` | No-op when nobody is signed in |
| `getAccessToken` | `() => Promise<string \| null>` | 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 `isReady` is `false` makes an already-authenticated user
flash through a login screen on every cold start.

## Step 5: sign in with 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 — `useMobileWallet().account` must be defined, since
`signMessages` triggers its own authorization otherwise.

Three encoding details decide whether this works:

1. **Pass `account.address`, which is base58.** `account.addressBase64` also 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 — `@wallet-ui/react-native-kit`
   has already done that conversion for you.
2. **`fromUint8Array` produces base64, not base58.** It is a re-export of `js-base64`. Privy
   wants the base64 string here; base58 fails verification.
3. **Do not slice the bytes.** `signMessages` 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.

`from.domain` is an RFC 3986 authority: a bare host, no scheme and no path. `from.uri` is a
full URI and is normally your app's deep link. Keep both stable — they are embedded in the
signed message the user sees in their wallet.

Linking a wallet to an account that already exists, and verifying the session on a server:
[references/siws.md](references/siws.md).

## Step 6: sign out of both

```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. `disconnect()` alone
keeps a live Privy session with no wallet behind it; `logout()` alone leaves the wallet
authorized and re-signs in silently on the next attempt.

## Which side owns what

| Concern | Owner |
| --- | --- |
| Private keys and signing | The wallet app, over MWA |
| Connected address | `useMobileWallet().account` |
| User identity across devices | `usePrivy().user` |
| Server-verifiable session | `usePrivy().getAccessToken()` |
| Sending transactions | `useMobileWallet().sendTransactions` |

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.

## Reference material

- [references/setup.md](references/setup.md) — polyfills, Metro config, `app.json` plugins,
  environment variables, and how to verify each one landed
- [references/siws.md](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](references/troubleshooting.md) — Privy-specific failures and
  their causes

The patterns here follow
[`expo-kit-privy`](https://github.com/solana-mobile/templates/tree/main/mobile/expo-kit-privy),
a complete working app. Read it when this file is ambiguous:

```bash
npx solana-mobile@latest create /tmp/reference-app --template expo-kit-privy --skip-install
```

## Related skills

- `solana-mobile-wallet` — MWA connection, signing, and sending, which this builds on
- `solana-mobile` — development builds, emulators, toolchain checks
- `seeker-genesis-token` — SIWS verified server-side without Privy, when a JWT is overkill

## 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

© solana-mobile · Apache-2.0 · solana-mobile/solana-mobile-skills

Похожие скиллы

Всё по теме «Мобильная разработка» →
clerk-android clerk Скилл помогает агенту подключить Clerk к нативному Android-приложению на Kotlin и Jetpack Compose — через готовые AuthView/UserButton или свой поток авторизации. CX Codex +1 14 400установок expo-native-ui expo Настраивает правила вёрстки нативных экранов в Expo по гайдам Apple HIG: цвета, элементы @expo/ui, SF Symbols и адаптивные отступы. CX Codex +1 31 700установок expo-brownfield expo Помогает внедрить React Native и Expo в существующие нативные приложения на iOS и Android. Пригодится для точечной встройки экранов в SwiftUI, UIKit или Kotlin. CX Codex +1 27 900установок react-native-brownfield-migration callstackincubator Помогает пошагово внедрять React Native или Expo в нативные приложения iOS и Android с помощью @callstack/react-native-brownfield. CX Codex +1 5 200установок dt-setup-react-native Dynatrace Пошагово настраивает плагин Dynatrace в проектах на React Native или Expo: правит конфиги Babel, подключает зависимости и проверяет параметры приватности. CX Codex +1 860установок react-native-patterns spencerpauly Скилл направляет агента при разработке мобильных приложений на React Native и Expo: от навигации до различий между iOS и Android. CX Codex +1 399установок

Из того же набора6

seeker-domains Разбирает имена .skr в адреса кошельков и обратно, чтобы в профилях и списках друзей вместо обрезанных адресов стояли читаемые имена. CX Codex +1 11 600установок seeker-genesis-token Проверяет владение устройством Seeker через Sign-in-with-Solana и серверный поиск токена SGT. Помогает настроить закрытый доступ к наградам или защиту от мультиаккаунтов. CX Codex +1 11 600установок solana-mobile Скилл проводит через создание, настройку и отладку Solana Mobile приложений на Android: шаблоны Expo, проверка тулчейна, эмуляторы, локальный валидатор. CX Codex +1 11 700установок seeker-connect Подключает веб-, React Native- и Kotlin-приложения к встроенному кошельку на телефоне Seeker: вход через Solana, подпись сообщений и транзакций. CX Codex +1 7 000установок solana-mobile-publishing Помогает собрать, подписать и выпустить Android APK в Solana dApp Store через dapp-store CLI. Пригодится при первой публикации, выпуске обновлений и минте App NFT. CX Codex +1 6 700установок

Спонсоры проекта

Компании, которые помогают держать аналитику открытой