For the complete documentation index, see llms.txt. This page is also available as Markdown.

API 使用模式

本文处理的是在 C# 中调用 SDK API 时反复遇到的模式。不是单个 API 具体做什么, 而是无论调用哪个 API 都同样适用的规则进行了汇总。

API 原文在哪里

Runtime/SDK/的 C# 表面由客户端 SDK(@apps-in-toss/web-framework)的类型定义自动生成。当前 有 85 个 API,分布在 24 个分类中

在哪里
什么

Unity IntelliSense

单个 API 的说明、参数、返回值。上位 SDK 的 JSDoc 已迁移为 C# XML 注释

平台政策、控制台设置、服务器联动等客户端 SDK 官方文档

这套文档集

上面两者都没有的 Unity 特有情况

本仓库的文档之所以不单独放 API 参考,是因为那会变成上位文档的手写副本。C# 表面会在每次更新 SDK 时重新生成,但手写的 Markdown 不会,因此时间一久必然会偏离。相反, IntelliSense 始终是最新的,而本文只写上位文档没有覆盖的内容——async/await、 AwaitableTask的分支, timeoutMs, AITException.ErrorCode,Mock(Editor mock、devtools)、IL2CPP stripping。

根据 SDK 版本,C# 表面如何变化,可以在 API 变更历史中查看。

基本模式

SDK API 是异步的。 await等待结果时不会阻塞 Unity 主线程。

using AppsInToss;
using UnityEngine;

public class Example : MonoBehaviour
{
    async void Start()
    {
        // 使用 await 关键字等待异步结果
        string deviceId = await AIT.GetDeviceId();
        Debug.Log($"Device ID: {deviceId}");
    }
}

重要: 有一个例外。应用内支付的 ProcessProductGrant 回调是唯一同步 bool返回的。原因和正确结构见下方 应用内支付:发放批准与服务器验证 一节。

Awaitable 和 Task

即使是同一个 API,随着 Unity 版本不同,返回类型也不同。

Unity 版本
返回类型

6000.0 及以上

Awaitable, Awaitable<T>

及以下

Task, Task<T>

await以它们消费的代码在两边都能直接运行,所以大多数情况下无需在意。只有在 显式写出返回类型时 才会分岔。

如果需要同时支持两个版本且必须有返回类型,就用条件编译分开。

参考: Task.WhenAll只在 TaskUnity 6 及以上 Awaitable可用,在别的版本没有。若要在 Unity 6 及以上同时推进多个 API,就用下面的方法。

调用多个 API

顺序调用就直接 await 接着写下去。

如果彼此独立,就先全部启动,之后再分别等待,这样往返会重叠。此方式在 AwaitableTask 两边都同样有效。

超时

所有异步 API 的最后一个参数都接收 timeoutMs。默认值是 0只在 无限等待

这个超时 只放弃 C# 侧的等待。 桥另一侧的 JavaScript 和平台任务仍可能继续执行,晚到的结果会被丢弃。因此,给有副作用的 API(支付、分享、权限请求等)设置超时时,不要把“超时 = 未执行”当成定论。

AITClientTimeoutException只在 继承自 AITException,因此现有的 catch (AITException) 块可以直接接住。只有想单独处理超时时才先捕获它。 ErrorCodeTIMEOUT

应用内支付:发放批准与服务器验证

IAPCreateOneTimePurchaseOrder / IAPCreateSubscriptionPurchaseOrder传给它的 ProcessProductGrant 回调会将发放与否 bool同步返回为 。关键是不要在这个回调里验证——回调要立即批准,服务器验证和实际发放则在覆盖层关闭后继续进行。核心 onEvent中进行。

这个回调不是可选的

ProcessProductGrant是可空字段,即使不指定也能编译, 但如果不指定,所有支付都会被当作发放失败。

JS 桥会把这个回调 始终 传给平台,所以如果在 C# 中没有注册处理器,SDK 就会在每次支付完成时自动返回 false。此时 Console 会留下如下错误:

接入支付流程时,先填这个字段。

为什么必须同步

支付覆盖层显示期间 visibilityState = hidden,因此 requestAnimationFrame会停止,单靠它运行的 Unity WebGL player loop 也会一起停止。所以在回调里 await的一个 continuation 会等待覆盖层关闭后才会到来的帧,而覆盖层又在等待该回调的响应,于是形成死锁。实机测量中,这个环路 持续了 115 秒 之后 "{应用名} 出现问题了。请申请退款" 页面出现了(支付成功后 30 秒内 true 没有响应就可能显示),而立即批准的支付在覆盖层 1.5 秒后关闭并正常完成。把返回类型固定为 bool,就是为了在编译阶段阻止这种 await 形态。

有两本账

回调返回值和我方服务器的发放记录是 两本不同的账

记录什么
所有权
截止

ProcessProductGrant 返回值

支付是否已消耗

Toss

30 秒(无帧)

我方服务器的发放记录

是否已交付商品

开发商

无截止,可重试

验证不是阻止 第一本账, 回调是回答“我已接收支付消耗”的地方,而验证和发放则留到之后从容进行。

所以,放进这个回调里的代码其实几乎就定成一行。

第 1 步回调立即批准

这个回调一旦被调用,就已经表示应用判定支付成功了。回调带来的信息只有 OrderId 一个,因此在这里也无法重新验证什么。

第 2 步验证与发放在 onEvent 中

服务器验证的 只有两个时点可以调用

  1. 正常流程下 onEvent ——覆盖层刚关闭后。

  2. 如果连这也错过了, 应用启动时的台词(第 3 步)。

onEvent之所以是第一个有效时点,是因为那是 OrderId和活着的 player loop 同时存在的最早瞬间。下面是实机测量的某次支付时间线。

onEvent开始之后,帧就以正常速度运行了, await可以尽情使用WaitForSecondsRealtime(0.2f)(在 202ms 完成)。

注意: SuccessEvent.Data里没有 Sku。商品是什么,只能通过当初开始购买时传入的 sku闭包捕获,或者由服务器 OrderId查询。

服务器验证什么

不能直接相信客户端发来的 OrderId。开发商服务器会通过 订单状态查询 API直接向 Toss 确认。

  • 必须使用 mTLS 证书(服务器之间通信)。证书和用户认证头的说明请参考 认证文档

  • x-toss-user-key 如果在头部里放入通过 Toss 登录获得的 userKey ,就只会响应该用户的订单 。不填的话会查询所有订单,因此若要防止拦截并重用其他用户的 OrderId,就必须一并发送这个头。

  • 响应里的 sku可以确认实际支付的商品。不要信任客户端告诉你的 SKU。

响应 status是这个 API 的核心。

status
含义

PURCHASED

支付和商品发放都已完成

PAYMENT_COMPLETED

支付已完成,但 商品发放失败

REFUNDED

退款完成

FAILED / ORDER_IN_PROGRESS / NOT_FOUND

支付失败 / 进行中 / 订单不存在

前两个值也就是 ProcessProductGrant 返回值的结果。 true返回的订单是 PURCHASED,否则的订单是 PAYMENT_COMPLETED

详细规范请见 官方 IAP 文档

第 3 步 应用启动时的未交付台词

不能保证第 2 步一定会执行。若在回调发送 true后应用立刻退出, onEvent就收不到 IAPGetPendingOrders中也不会出现。

回收这种情况的是 IAPGetCompletedOrRefundedOrders。在应用启动或回到前台时扫一遍,找出我方服务器还没发放的订单。

如果没有这第 3 步,第 1 步的立即批准就会变得危险。 这三者是一组。

重要:退款只能通过轮询得知。支付或退款发生时,并不会提供通知开发商服务器的 webhook。即使用户完成了退款,在应用再次运行并触发这段台词之前,开发商也无从得知。若要回收已退款订单的商品,就必须把已发放订单的 OrderId保存在服务器上,并通过订单状态查询 API 定期检查。

false 什么时候返回

官方文档说明, true对于 false 之外的响应,退款提示页面 可能会显示。 (我直接测量的是无响应路径,尚未确认显式 false时是否也会出现同样的画面。)因此 false只有在真的无法把这个商品发给用户时 才使用——比如在支付过程中,已拥有的非消耗品在别的设备上被获取,因而现在能断定无法发放的时候。

“因为没把握,所以先 false”这种说法不成立。那样会让应用每次支付都弹出退款提示。把握应通过 1~3 步获得, false而不是通过

参考:旧版 Toss 应用里会忽略返回值。 processProductGrant在不支持的版本(Android 5.231.1 以下 / iOS 5.230.0 以下)中,桥接会回退到旧支付路径,此时回调返回值不会传到平台,而是被丢弃。编写依赖返回值的逻辑时,请把这一区间考虑进去。

错误处理

如果 API 调用失败 继承自 AITException会抛出这个异常。

属性
类型
说明

Message

string

人类可读的错误消息

ErrorCode

string

错误代码。如果平台未提供,则为空字符串

APIName

string

失败的 API 名称。若未知则为空字符串

IsPlatformUnavailable

bool

是否因缺少平台桥接而产生的错误

ErrorCode若要据此分支,请注意值可能为空。

IsPlatformUnavailable

这个标志不是作为单独字段传递的,而是 根据错误消息来判定。如果下面字符串中任意一个出现 true就会变为 true。

判定字符串
何时

__GRANITE_NATIVE_EMITTER

没有原生 emitter

ReactNativeWebView

在 Toss 应用 WebView 外部运行中

不是常量处理器

该 API 没有桥接处理器

无法读取 undefined 的属性

window.AppsInToss尚未初始化

true如果是这样,就不是代码 bug,而是 运行环境问题。这在普通浏览器或开发环境中很常见,因此在上报错误时,最好将此情况降为较低严重性或过滤掉。

按运行环境的行为

环境
行为

WebGL 构建 + Apps in Toss 应用

实际原生 API 调用

WebGL 构建 + 普通浏览器

大多会失败。若 devtools 开启(Dev Server),则以 mock 响应

Unity Editor

Editor mock 调用

其他平台(Windows、macOS 等)

Editor mock 调用

Editor mock 与构建配置无关。 Runtime/SDK/的各个 API #if UNITY_WEBGL && !UNITY_EDITOR分叉成,因此如果不是 WebGL 构建 在编译时 只会保留 mock 路径。

如有需要,可以按运行环境分支。

若要确认实际原生行为,必须构建为 WebGL 并在 Apps in Toss 应用中运行。在 Editor 中无论做什么都是 mock。

模拟

被称为“Mock”的有 两种,并且它们的行为不同。

Editor mock
devtools

在哪里

Runtime/SDK/的 C#

@apps-in-toss/devtools(npm 包,在浏览器中打开构建产物时运行)

什么

所有 SDK API

60 多个 SDK API + 可操控状态的浮动面板

何时

不是 WebGL 构建时(在编译时决定)

在普通浏览器中打开通过 Dev Server 运行的构建时

如何关闭

无法关闭

AIT > Configuration的 devtools 设置,或服务器运行时的环境变量 AIT_DEVTOOLS=0

Editor mock

在 Unity Editor 和非 WebGL 平台上调用 API 时,会记录日志并返回默认值。由于不会抛出异常,所以在 Editor 中游戏逻辑不会停止。

返回类型
Mock 返回值

string

空字符串 ""

bool

false

数组

空数组

类类型

default,也就是 null

取消订阅 Action

仅记录日志的函数。 SafeAreaInsetsSubscribenull

类类型为 null会以该形式传来,这一点很重要。在 Editor 中 result.SomeField如果直接读取 空引用异常如果要在 Editor 中也运行这段逻辑,请加入 null 检查。返回数组的 API 会返回空数组,因此 foreach 是安全的。 foreach是安全的。

devtools

@apps-in-toss/devtools@apps-in-toss/web-framework 仅限 3.x 是开发工具。运行 Dev Server 时,vite 插件会 @apps-in-toss/web-framework 将 import 别名到 mock 实现,在没有 Toss 应用的普通浏览器中,60 多个 SDK API 会以 mock 方式运行。同时屏幕上会出现浮动面板,可以直接操作登录状态·广告结果·存储值等 mock 状态。

面板默认开启。若要关闭整个 devtools(或仅面板) AIT > Configuration请更改 devtools 设置——因为构建产物保持不变, 只需重启服务器即可生效。如果像 CI 或临时确认那样,只想在不改设置的情况下临时关闭一次,可以用服务器运行环境变量 AIT_DEVTOOLS=0进行覆盖。

在 devtools 关闭的状态下(例如在普通浏览器中打开但禁用了 devtools 的情况)调用 SDK API 时, IsPlatformUnavailabletrue继承自 AITException会发生。

相关文档

这有帮助吗?