> ## Documentation Index
> Fetch the complete documentation index at: https://docs.monad.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# 如何在 React Native 中使用 Mera

> 在 React Native 应用中创建和复用由 passkey 派生的 Monad 账户。

[Mera](https://mera.category.xyz/) v0.2.0 通过原生 WebAuthn 客户端添加了 React Native 支持。用户可以在 Web 上创建 passkey，在 iOS 或 Android 上使用同步的 passkey 登录，并在每个应用中派生出相同的 Monad 账户。

Web 应用和移动应用必须使用相同的 relying party ID（`rpId`）。passkey 提供商还必须将 passkey 同步到移动设备并支持 WebAuthn PRF 扩展。

<Note>
  [React Native 演示](https://github.com/category-labs/mera/tree/main/demos/mobile) 展示了完整流程，包括 passkey 登录、由生物识别把关的设备存储、账户恢复以及交易签名。
</Note>

## 前置条件

* Node.js 24 或更高版本
* 已初始化的 Expo 项目，且已安装 `expo`
* `@category-labs/mera` v0.2.0 或更高版本
* 您自己控制的 HTTPS 主机，用于 relying party ID 和平台关联文件
* iOS 18 或更高版本，或 Android 9 或更高版本，且 passkey 提供商支持 PRF
* iOS 使用 Xcode，Android 使用 Android Studio，并在 `PATH` 中提供带有 `keytool` 的 JDK

下面的 Expo 示例使用 development build，因为 `react-native-passkey` 会调用原生平台 API。

## 安装

从现有 Expo 项目的根目录运行以下命令。如果要创建新项目：

```bash theme={null}
npx create-expo-app@latest my-monad-app --template blank-typescript --yes
cd my-monad-app
```

创建项目的命令会安装 `expo` 并生成本指南中使用的应用入口点。您无需克隆 Mera 演示。

安装 Mera、[`react-native-passkey`](https://github.com/f-23/react-native-passkey) 客户端以及其他依赖：

```bash theme={null}
npm install @category-labs/mera react-native-passkey viem @scure/bip32 @scure/bip39
npx expo install expo-crypto
```

## 添加 Hermes crypto polyfill

Mera 需要 `crypto.getRandomValues`，而 Hermes 并未提供。使用 `expo-crypto` 创建一个 polyfill：

```ts title="src/polyfills.ts" theme={null}
import { getRandomValues } from "expo-crypto";

if (typeof globalThis.crypto?.getRandomValues !== "function") {
  Object.defineProperty(globalThis, "crypto", {
    configurable: true,
    value: { ...globalThis.crypto, getRandomValues },
  });
}
```

在使用 Mera 的代码被导入之前，从应用入口点导入该 polyfill：

```ts title="index.ts" theme={null}
import "./src/polyfills";

import { registerRootComponent } from "expo";
import App from "./App";

registerRootComponent(App);
```

## 将应用与 passkey 域名关联

为 relying party ID 选择一个主机，例如 `accounts.example.com`。仅使用主机名，不要包含 `https://` 或路径。

要复用 Web 应用创建的 passkey，Web 应用必须使用相同的 `rpId` 来创建它。每个移动平台还需要一个公开的关联文件，用于授权原生应用为该主机使用凭据。

### iOS

从 `https://accounts.example.com/.well-known/apple-app-site-association` 提供以下 JSON：

```json theme={null}
{
  "webcredentials": {
    "apps": ["TEAM_ID.com.example.app"]
  }
}
```

将 `TEAM_ID` 替换为您的 Apple team ID，将 `com.example.app` 替换为应用的 bundle identifier。将关联域名添加到 Expo 配置：

```ts title="app.config.ts" theme={null}
import type { ExpoConfig } from "expo/config";

const rpId = "accounts.example.com";

const config: ExpoConfig = {
  name: "My Monad App",
  slug: "my-monad-app",
  ios: {
    bundleIdentifier: "com.example.app",
    associatedDomains: [`webcredentials:${rpId}`],
  },
  android: {
    package: "com.example.app",
  },
  extra: { rpId },
};

export default config;
```

### Android

从 `https://accounts.example.com/.well-known/assetlinks.json` 提供以下 JSON：

```json theme={null}
[
  {
    "relation": ["delegate_permission/common.get_login_creds"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.app",
      "sha256_cert_fingerprints": ["SHA256_FINGERPRINT"]
    }
  }
]
```

将 package name 和 fingerprint 替换为 Android 应用对应的值。包含用于签署应用的每个证书的 fingerprint。下一节说明如何打印默认 Expo debug 证书的 fingerprint。

## 构建原生应用

配置应用后，生成 `ios/` 和 `android/` 项目。Expo Prebuild 会创建这些目录，并将 `react-native-passkey` 与原生项目链接：

```bash theme={null}
npx expo prebuild
```

对于 Android development build，打印生成的 debug 证书的 fingerprint，并在 `assetlinks.json` 中用该结果替换 `SHA256_FINGERPRINT`：

```bash theme={null}
keytool -list -v \
  -keystore android/app/debug.keystore \
  -alias androiddebugkey \
  -storepass android \
  -keypass android
```

在测试 passkey 之前，使用该 fingerprint 更新并部署 `assetlinks.json`。两个平台关联文件都必须通过 HTTPS 公开可用，返回 JSON，并且不经过重定向进行响应。

然后编译并运行 development build。iOS：

```bash theme={null}
npx expo run:ios
```

Android：

```bash theme={null}
npx expo run:android
```

Expo Go 无法运行此应用，因为它不包含原生 passkey 模块。

## 创建或复用 passkey

React Native 不暴露浏览器的 WebAuthn API。将 Mera 的 `reactNativeWebAuthnClient` 传递给每个运行 passkey ceremony 的函数：

```ts title="src/passkey.ts" theme={null}
import {
  createPasskeyWithPrfOutput,
  getPasskeyPrfOutput,
} from "@category-labs/mera";
import { reactNativeWebAuthnClient } from "@category-labs/mera/react-native-webauthn-client";

const rpId = "accounts.example.com";

export function createAccount() {
  return createPasskeyWithPrfOutput({
    rp: { id: rpId, name: "My Monad App" },
    user: { name: "player@example.com", displayName: "Player One" },
    webAuthnClient: reactNativeWebAuthnClient,
  });
}

export function signIn() {
  return getPasskeyPrfOutput({
    rpId,
    webAuthnClient: reactNativeWebAuthnClient,
  });
}
```

调用 `signIn` 时不带 credential ID，可以让平台提供该 relying party 可用的任何 passkey。如果在 Web 上创建的 passkey 已同步到设备，选择它将返回相同的 PRF 输出，从而派生出相同的账户。

## 派生 Monad 账户

从返回的 PRF 输出派生标准 EVM BIP-44 密钥：

```ts title="src/wallet.ts" theme={null}
import { HDKey } from "@scure/bip32";
import { entropyToMnemonic, mnemonicToSeedSync } from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english.js";

export function deriveEvmKey(
  prfOutput: Uint8Array,
  index = 0,
): Uint8Array {
  const mnemonic = entropyToMnemonic(prfOutput, wordlist);
  const seed = mnemonicToSeedSync(mnemonic);
  try {
    const node = HDKey.fromMasterSeed(seed).derive(`m/44'/60'/0'/0/${index}`);

    if (node.privateKey === null) {
      throw new Error("Derivation produced no private key");
    }

    return node.privateKey;
  } finally {
    seed.fill(0);
  }
}
```

派生路径必须与 Web 应用一致。仅当您打算从同一 passkey 使用另一个账户时才递增 `index`。

## 发送交易

创建一个签名会话，将其适配为 viem 账户，并在 Monad 上发送交易：

```ts title="src/send-transaction.ts" theme={null}
import { createSecp256k1SigningSession } from "@category-labs/mera";
import { toViemAccount } from "@category-labs/mera/viem";
import { createWalletClient, http, parseEther } from "viem";
import { monadTestnet } from "viem/chains";
import { signIn } from "./passkey";
import { deriveEvmKey } from "./wallet";

export async function sendTransaction() {
  const { prfOutput } = await signIn();
  const privateKey = deriveEvmKey(prfOutput);
  const session = createSecp256k1SigningSession({ privateKey });

  try {
    const client = createWalletClient({
      account: toViemAccount(session),
      chain: monadTestnet,
      transport: http(),
    });

    const hash = await client.sendTransaction({
      to: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
      value: parseEther("0.01"),
      gas: 21_000n,
    });

    return hash;
  } finally {
    privateKey.fill(0);
    prfOutput.fill(0);
    session.end();
  }
}
```

主网请使用 `monad` 代替 `monadTestnet`。使用其他交易类型之前，请参见 [Gas 定价](/zh/developer-essentials/gas-pricing)。

## 存储和解锁账户

credential ID 是元数据，不包含密钥材料。而 PRF 输出可以派生账户的私钥，必须作为敏感密钥材料对待。

为获得更流畅的移动体验，应用可以将 PRF 输出存储在受生物识别或设备凭据保护的平台安全存储中。这样应用无需在每次交易时都显示 passkey 选择器即可解锁现有账户。

* 不要将 PRF 输出存储在 `AsyncStorage` 或应用日志中。
* 当用户锁定钱包、退出登录或应用会话过期时，结束签名会话。
* 派生会话后清除临时的 PRF、seed 和私钥缓冲区。
* 在显示恢复短语或执行其他敏感导出之前，再次请求 passkey。

[移动演示存储实现](https://github.com/category-labs/mera/blob/main/demos/mobile/src/storage.ts) 使用 Expo SecureStore 展示了此模式。

## 故障排查

* **`PRF_UNAVAILABLE`**：所选的 passkey 提供商或操作系统未返回 PRF 扩展。请检查 [Mera 的 Authenticator 支持](https://mera.category.xyz/authenticator-support/)。
* **首次请求出现 `PASSKEY_OPERATION_FAILED`**：确认关联文件可无重定向访问，并且包含已安装应用的准确 bundle 或 package 标识符及签名证书。
* **移动端地址与 Web 地址不同**：确认两个应用使用相同的 `rpId`、相同的 passkey 以及相同的 BIP-44 账户索引。

## 延伸阅读

* [在 Web 上使用 Mera 创建 passkey 账户](/zh/guides/mera)
* [官方 React Native 集成教程](https://mera.category.xyz/recipes/use-mera-with-react-native/)
* [React Native 演示](https://github.com/category-labs/mera/tree/main/demos/mobile)
* [Mera WebAuthn 客户端参考](https://mera.category.xyz/reference/web-authn-client/)
