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

# 应用内购买

> 使用 @elata-biosciences/app-payments 在应用中出售一次性物品。

## 概述

`@elata-biosciences/app-payments` 让你的应用可以打开 Elata 的结账流程，并查询用户拥有哪些物品。Elata 负责结账、以 USDC 收款并记录所有权。你的应用永远不会接触钱包或支付信息。

```bash theme={null}
npm install @elata-biosciences/app-payments
```

| 函数 | 返回值 | 用途 |
| - | - | - |
| `getCatalog(opts?)` | `Promise<CatalogItem[]>` | 列出应用的物品，包括价格、标题和描述 |
| `getOwnedItems(opts?)` | `Promise<number[]>` | 列出用户拥有的所有 `contentId` |
| `hasItem(contentId, opts?)` | `Promise<boolean>` | 检查单个物品 |
| `requestPurchase(input)` | `Promise<PurchaseResult>` | 为某个物品打开结账 |

<Note>
  你的物品目录由 Elata 团队配置。请联系我们，提供你想出售的物品、标题和价格。
</Note>

***

## 正确的流程

先根据所有权控制界面，再购买，然后应用权益。

```ts theme={null}
import {
  getCatalog,
  getOwnedItems,
  requestPurchase,
  AppPaymentsError,
} from '@elata-biosciences/app-payments';

const formatUsdc = (base: string) => `$${(Number(base) / 1e6).toFixed(2)}`;

// 1. 读取目录以及用户已拥有的物品。
const [items, owned] = await Promise.all([getCatalog(), getOwnedItems()]);
const ownedSet = new Set(owned);

// 2. 启动时重新应用权益。Elata 是唯一可信来源，
//    因此刷新页面或更换设备后依然有效。
for (const id of owned) applyBenefit(id);

// 3. 购买，然后根据 status 分支处理。
async function buy(item) {
  try {
    const result = await requestPurchase({
      contentId: item.contentId,
      priceUsdc: formatUsdc(item.priceUsdc), // 仅用于显示
      title: item.title,
    });
    if (result.status === 'success') {
      ownedSet.add(item.contentId);
      applyBenefit(item.contentId);
    } else if (result.status === 'error') {
      showError(result.error);
    }
    // 'cancelled'：用户取消了，不做任何处理。
  } catch (err) {
    // 仅在本地问题时抛出：没有父框架、输入错误、超时。
    showError(err instanceof AppPaymentsError ? err.code : String(err));
  }
}
```

`requestPurchase` 只记录用户*拥有*某个物品。拥有它*意味着什么*由你的应用决定。

***

## 注意事项

<AccordionGroup>
  <Accordion title="根据 status 分支，而不是 txHash">
    如果用户已经拥有该物品，Elata 会跳过支付并返回 `{ status: "success", txHash: "" }`。检查 `if (result.txHash)` 的代码会错误处理这种情况。
  </Accordion>

  <Accordion title="两种价格格式">
    `CatalogItem.priceUsdc` 是 USDC 的**最小单位**（6 位小数）：`"50000"` 表示 \$0.05。`RequestPurchaseInput.priceUsdc` 是 Elata 会忽略的自由格式显示提示。Elata 始终按目录价格收费。
  </Accordion>

  <Accordion title="物品是一次性解锁">
    每个物品只能购买一次。物品不支持消耗品（按次扣减的余额）或周期性计费。如需持续收入，请使用[订阅计划](/cn/apps/platform/subscriptions)。
  </Accordion>

  <Accordion title="超时">
    `requestPurchase` 最多等待 5 分钟让用户完成结账。`getCatalog`、`getOwnedItems` 和 `hasItem` 默认 10 秒。在 Elata 之外没有人会回复，因此请将 `timeout` 处理为“显示物品可购买”。
  </Accordion>
</AccordionGroup>

***

## 错误

调用只会以带有 `code` 的 `AppPaymentsError` 拒绝：

| Code | 含义 |
| - | - |
| `invalid_input` | `contentId` 不是非负整数，或某个选项类型错误 |
| `no_window` | 不在浏览器中运行 |
| `no_parent` | 不在 Elata 框架内运行 |
| `no_crypto` | `crypto.randomUUID` 不可用 |
| `timeout` | 未及时收到回复 |
| `not_authenticated` | 用户未登录（所有权查询） |
| `fetch_failed` | Elata 无法查询所有权 |

正常的 `cancelled` 或 `error` 购买结果会被 resolve，而不是 reject。

***

## 安全模型

* 你的应用**无法伪造所有权**。所有权只有在支付验证通过后才由 Elata 记录。
* 你的应用**永远看不到**支付信息、钱包私钥或会话令牌。
* 使用相同的 `requestId` 重复请求不会重复扣费。
* 物品销售收入支付到应用所有者的钱包。

***

## 动手试试

* [`examples/iap-demo`](https://github.com/Elata-Biosciences/elata-bio-sdk/tree/main/examples/iap-demo)：一个自包含的 HTML 文件，内置模拟 Elata，可在本地运行完整流程。
* [npm 上的包 README](https://www.npmjs.com/package/@elata-biosciences/app-payments)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.