diff --git a/cn/docs/sdk/_partials/unity-sdk-installation.mdx b/cn/docs/sdk/_partials/unity-sdk-installation.mdx index aa9158690..8285a66d6 100644 --- a/cn/docs/sdk/_partials/unity-sdk-installation.mdx +++ b/cn/docs/sdk/_partials/unity-sdk-installation.mdx @@ -31,30 +31,6 @@ SDK 内部使用了部分第三方库,开发者接入时需先确保 SDK 外 } ``` -:::tip - -如果需要接入 TapSDK Unity PC 平台,需要在使用 NuGet 集成工具,集成 `K4os.Compression.LZ4` , `protobuf-net` 两个库,具体设置如下: - -- 在 `Packages/manifest.json` 添加如下依赖: -```json -"com.github-glitchenzo.nugetforunity": "https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity" -``` -- 添加完后,在 Unity 顶部菜单中如果没有出现 **NuGet**选项,可以重启 Unity 编辑器,包括 Unity Hub 客户端。 -- 在 Unity 顶部菜单中选择 **NuGet > Manage NuGet Packages**,在弹出的窗口中搜索 `K4os.Compression.LZ4` 和 `protobuf-net` 并安装。 - - 最终会在 Assets文件夹下的 packages.config 文件中自动生成如下依赖: -```xml - - - - - - - - -``` -::: - ### 添加 SDK 依赖 SDK 支持** Unity Package Manager ** 及本地文件导入两种集成方式,开发者根据需求选择其中一种即可,推荐使用远程依赖。 diff --git a/cn/docs/sdk/embedded-moments/guide.mdx b/cn/docs/sdk/embedded-moments/guide.mdx index 5f6458427..9592975ef 100644 --- a/cn/docs/sdk/embedded-moments/guide.mdx +++ b/cn/docs/sdk/embedded-moments/guide.mdx @@ -71,11 +71,13 @@ import v4SDKVersions from '/src/docComponents/v4SDKVersions'; 使用内嵌动态服务需要相册、相机权限,所以开发者需在工程的 `info.plist` 配置相关权限并**替换授权文案**: ```xml - + NSPhotoLibraryUsageDescription 说明为何应用需要此项权限 NSCameraUsageDescription 说明为何应用需要此项权限 +NSMicrophoneUsageDescription +说明为何应用需要此项权限 ``` @@ -130,11 +132,13 @@ unitypackageModules={[ - + NSPhotoLibraryUsageDescription 说明为何应用需要此项权限 NSCameraUsageDescription 说明为何应用需要此项权限 + NSMicrophoneUsageDescription + 说明为何应用需要此项权限 ``` @@ -702,7 +706,7 @@ let postData = TapTapMomentImageData() // 设置发布的文字内容 postData.content = "动态文字内容" // 设置发布的图片地址 -let imagePath = "file://..." +let imagePath = "/var/mobile/**.png" postData.images = [imagePath] // 发布图文动态 TapTapMoment.publish(postData) diff --git a/cn/docs/sdk/start/agreement.mdx b/cn/docs/sdk/start/agreement.mdx index 8a34a0d51..05a43ea7f 100644 --- a/cn/docs/sdk/start/agreement.mdx +++ b/cn/docs/sdk/start/agreement.mdx @@ -2,9 +2,8 @@ title: TapSDK 隐私政策 sidebar_position: 8 --- - -更新日期:2024 年 9 月 30 日 -生效日期:2024 年 9 月 30 日 +更新日期:2024 年 11 月 15 日 +生效日期:2024 年 11 月 15 日 易玩(上海)网络科技有限公司(以下简称“TapTap”或“我们”)通过 TapSDK 向开发者提供多种服务,开发者可以根据自身需求在其应用中接入其中任意一项或多项服务。本文档将向开发者和其用户(以下或称“玩家”)说明 TapSDK 的隐私安全信息,包括 TapSDK 各项服务处理的个人信息范围、处理目的、权限使用情况等。 @@ -381,7 +380,7 @@ sidebar_position: 8 个人信息/权限类型 个人信息/权限名称 - 使用目的 + 使用目的 必要个人信息/权限 @@ -425,7 +424,7 @@ sidebar_position: 8 个人信息/权限类型 个人信息/权限名称 - 使用目的 + 使用目的 必要个人信息/权限 @@ -460,6 +459,48 @@ sidebar_position: 8 +**1.2.10 TapTap IAP** + +1)功能介绍:提供 TapTap IAP(In App Purchase)能力,玩家可以通过 TapTap IAP 快速购买所需要的游戏道具,最终会通过微信支付、支付宝支付等方式完成支付。 + +2)收集信息/获取权限 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
个人信息/权限类型个人信息/权限名称使用目的
必要个人信息/权限系统版本为了确保设备系统兼容、定位解决问题
设备型号
设备 CPU 信息
网络类型
AndroidID
设备内存信息
手机样式
可选个人信息/权限/
+ + 1.3 我们仅为实现 TapSDK 产品和/或服务功能,对所收集的用户个人信息进行处理。若需要将收集的个人信息用于其他目的,我们会以合理方式告知用户,并在获得用户的同意后进行使用。 @@ -503,7 +544,7 @@ sidebar_position: 8 3.2 转让 -通常情况下,除非获得用户的明确同意,我们不会将用户的个人信息转让给任何公司、组织和个人,但以下情况除外: +通常情况下,除非获得用户的明确同意,我们不会将用户的个人信息转让给任何公司、组织和个人,但以下情况除外: 在涉及合并、收购、资产转让或类似的交易时,如涉及到个人信息转让,我们会要求新的持有用户个人信息的公司、组织以不低于本隐私政策所要求的标准继续保护用户的个人信息,否则,我们将要求该公司、组织重新向用户征求授权同意。 3.3 披露 @@ -602,4 +643,10 @@ TapSDK 个人信息保护负责人邮箱:`privacy@taptap.com` 我们将在 15 天内予以回复。 -本页面内容具有多种语言版本,若其他语言版本与简体中文版本发生冲突,应以简体中文版本为准。 \ No newline at end of file +本页面内容具有多种语言版本,若其他语言版本与简体中文版本发生冲突,应以简体中文版本为准。 + + + + + + diff --git a/cn/docs/sdk/start/compliance.mdx b/cn/docs/sdk/start/compliance.mdx index ab521f4fc..6d79cb986 100644 --- a/cn/docs/sdk/start/compliance.mdx +++ b/cn/docs/sdk/start/compliance.mdx @@ -406,6 +406,50 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in 无 +### 10. TapTap IAP + +- 功能介绍 + +提供 TapTap IAP(In App Purchase) 能力,玩家可以通过 TapTap IAP 快速购买所需要的游戏道具,最终会通过微信支付、支付宝支付等方式完成支付。 + +- 合规调用时机 + +玩家触发游戏内道具购买时进行初始化和调用。 + +- 需要权限 + +| 权限 | 使用目的 | 权限申请时机 | +| ---------------------- | ---------------------- | ---------------------- | +| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | +| 获取网络状态 | 用于检测当前网络连接是否有效 | 用户首次使用该功能时会申请权限 | + +- 关闭功能的配置方式 + +若要关闭,可在app目录build.gradle中移除对该功能的依赖(详见[文档](https://developer.taptap.cn/docs/sdk/update/guide/#tapsdk-%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: + +``` +// implementation("com.taptap.android.payment:iap:latest") +// implementation("com.taptap.android.payment:base:latest") +// implementation("com.taptap.android.payment:alipaycn:latest") +// implementation("com.taptap.android.payment:wechat:latest") +``` + +- 必要个人信息 + +| 必要个人信息 | 使用目的 | 场景 | 收集频次 | +| ------ | ------ | ------ | ------ | +| 系统版本 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | +| 设备型号 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | +| 设备 CPU 信息 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | +| 网络类型 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | +| Android ID | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 初始化及用户发起授权时获取一次 | +| 设备内存信息 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | +| 手机样式 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | + +- 可选个人信息 + +无 + ## **四、向最终用户披露 TapSDK 条款** diff --git a/cn/docs/sdk/start/overview.mdx b/cn/docs/sdk/start/overview.mdx index 537879eee..cb2ba4f8b 100644 --- a/cn/docs/sdk/start/overview.mdx +++ b/cn/docs/sdk/start/overview.mdx @@ -52,6 +52,8 @@ TDS 提供以下技术服务,开发者可以通过在游戏中集成 TapSDK - **APK 加固**:避免游戏包体被破解篡改,保障游戏安全。 +- **内购服务**:TapTap 游戏内购服务为开发者提供了便捷高效的解决方案。接入该服务后,开发者可轻松开放游戏内购,无论是消耗型道具还是非消耗型道具,都能方便地上架售卖。同时,订单查看功能,让开发者对销售情况一目了然。 + 使用对应的服务请先完成[开发者注册](https://developer.taptap.cn/)[开发者注册](https://developer.taptap.io/),之后登录开发者中心开启「游戏服务」。 diff --git a/cn/docs/sdk/start/release-notes/android.mdx b/cn/docs/sdk/start/release-notes/android.mdx index c9c92f9b9..f7334cccb 100644 --- a/cn/docs/sdk/start/release-notes/android.mdx +++ b/cn/docs/sdk/start/release-notes/android.mdx @@ -3,6 +3,25 @@ title: Android sidebar_position: 2 --- +

4.5.0

+ +更新时间 2024-11-18 + +

功能

+ +- 新增 [ 内购服务 ] :提供整合商品管理、支付渠道、订单管理的便捷内购解决方案 + +

优化

+ +- 合规认证:当 Startup 接口异步处理未结束时,如果重复调用该接口,SDK 将直接返回 +- 删除冗余 Protobuf 依赖 + + +

修复

+ +- 修复 OAID 参数设置无效问题 + +

4.4.3

更新时间 2024-10-30 diff --git a/cn/docs/sdk/start/release-notes/ios.mdx b/cn/docs/sdk/start/release-notes/ios.mdx index b0ae4d308..9ed84a3aa 100644 --- a/cn/docs/sdk/start/release-notes/ios.mdx +++ b/cn/docs/sdk/start/release-notes/ios.mdx @@ -3,6 +3,21 @@ title: iOS sidebar_position: 3 --- +

4.5.0

+ +更新时间 2024-11-18 + +

优化

+ +- 合规认证:当 Startup 接口异步处理未结束时,如果重复调用该接口,SDK 将直接返回 +- 内嵌动态:添加隐私说明文件,删除在 framework 中冗余的资源文件 + +

修复

+ +- 合规认证:修复使用原生接入时编译异常 +- 内嵌动态:修复使用原生接入时编译异常 + +

4.4.3

更新时间 2024-10-30 diff --git a/cn/docs/sdk/start/release-notes/unity.mdx b/cn/docs/sdk/start/release-notes/unity.mdx index 862565c36..26d114fff 100644 --- a/cn/docs/sdk/start/release-notes/unity.mdx +++ b/cn/docs/sdk/start/release-notes/unity.mdx @@ -3,6 +3,27 @@ title: Unity sidebar_position: 1 --- +

4.5.0

+ +更新时间 2024-11-18 + +

功能

+ +- [Android] 新增 [ 内购服务 ] :提供整合商品管理、支付渠道、订单管理的便捷内购解决方案 + + +

优化

+ +- 合规认证:当 Startup 接口异步处理未结束时,如果重复调用该接口,SDK 将直接返回 +- 删除冗余 Protobuf 依赖 +- [iOS] 内嵌动态:添加隐私说明文件,删除在 framework 中冗余的资源文件 +- [PC] TapTap 登录:更新内部网络库及登录页面扫码引导图 + +

修复

+ +- [Android] 修复 OAID 参数设置无效问题 +- [Android] 数据分析:修复调用仅包含用户 ID 的 SetUserID 接口时的异常问题 +

4.4.3

更新时间 2024-10-30 diff --git a/cn/docs/sdk/tap-iap/_category_.json b/cn/docs/sdk/tap-iap/_category_.json new file mode 100644 index 000000000..9eb851869 --- /dev/null +++ b/cn/docs/sdk/tap-iap/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "内购服务", + "collapsed": true, + "position": 16 +} diff --git a/cn/docs/sdk/tap-iap/create-merchant.mdx b/cn/docs/sdk/tap-iap/create-merchant.mdx new file mode 100644 index 000000000..60b19410b --- /dev/null +++ b/cn/docs/sdk/tap-iap/create-merchant.mdx @@ -0,0 +1,121 @@ +--- +title: 创建商户 +sidebar_position: 2 +--- + +import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; + +## 准备材料 + +### 1. 营业执照 必填* + +* 内容要求:请提供彩色照片 or 彩色扫描件 or 加盖公章鲜章的复印件,要求正面拍摄,露出证件四角且清晰、完整,所有字符清晰可识别,不得反光或遮挡。不得翻拍、截图、镜像、PS。 +* 格式要求:只支持 JPG、BMP、PNG格式。 +* 大小要求:文件大小不能超过 2 M。 + +### 2. 法定代表人证件 必填* + +法定代表人证件可使用居民身份证或护照。 + +#### 居民身份证 + +* 内容要求:请提供身份证人像面照片及国徽面照片。请提供彩色照片 or 彩色扫描件 or 加盖公章鲜章的复印件,要求正面拍摄,露出证件四角且清晰、完整,所有字符清晰可识别,不得反光或遮挡。不得翻拍、截图、镜像、PS。 +* 格式要求:只支持 JPG、BMP、PNG格式。 +* 大小要求:文件大小不能超过 2 M。 + +#### 护照(可选) + +* 内容要求:请提供护照人像面照片。请提供彩色照片 or 彩色扫描件 or 加盖公章鲜章的复印件,要求正面拍摄,露出证件四角且清晰、完整,所有字符清晰可识别,不得反光或遮挡。不得翻拍、截图、镜像、PS。 +* 格式要求:只支持 JPG、BMP、PNG格式。 +* 大小要求:文件大小不能超过 2 M。 + +### 3. 受益人证件 + +如果公司法定代表人**并非最终受益人(UBO)**,请提供最终受益人证件信息。受益人证件要求同法定代表人证件,此处不赘述。 + +### 4. 网络游戏电子出版物审批(版号)必填* + +* 内容要求:请提供有效期内的游戏版号。请提供彩色照片 or 彩色扫描件 or 加盖公章鲜章的复印件,要求正面拍摄,露出证件四角且清晰、完整,所有字符清晰可识别,不得反光或遮挡。不得翻拍、截图、镜像、PS。 +* 格式要求:只支持 JPG、BMP、PNG格式。 +* 大小要求:文件大小不能超过 2 M。 +* 特殊说明:如果营业执照主体不是版号的运营单位,请提供授权书。 + +### 5. 计算机软件著作权登记证书(软著)必填* + +* 内容要求:请提供彩色照片 or 彩色扫描件 or 加盖公章鲜章的复印件,要求正面拍摄,露出证件四角且清晰、完整,所有字符清晰可识别,不得反光或遮挡。不得翻拍、截图、镜像、PS。 +* 格式要求:只支持 JPG、BMP、PNG格式。 +* 大小要求:文件大小不能超过 2 M。 +* 特殊说明:如果营业执照主体不是软著主体,请提供授权书。 + +### 6. IP 授权书 + +若游戏涉及 IP 合作,请确保已获得合法有效授权,并提供授权书。 + +### 7. 微信开放平台 AppID + +如需开通微信支付,必须提供[微信开放平台的 AppID](https://kf.qq.com/faq/181105JJNbmm181105eUZfee.html)。 + +* 提供应用的 AppID,将开通微信的 App 支付。 +* 提供公众号或小程序的 AppID,将开通微信的 JSAPI 支付。*目前 TapTap 游戏内购服务的 JSAPI 支付能力正在开发中,敬请期待!* + +### 8. App 截图 + +如需开通微信的 App 支付,必须提供以下所有的 App 截图: + +* 游戏商店页截图:游戏上架苹果、华为、小米等主流应用市场的游戏详情页截图,游戏需开放下载,截图需包括商店 Logo、游戏主体信息。TapTap 独家游戏提供 TapTap 游戏详情页截图即可。 +* 游戏首页。 +* 游戏尾页:尾页需包含版号、版号主体、软著等信息。 +* 游戏内截图。 +* 游戏支付页面:需含微信支付。 + +![](https://img.tapimg.com/market/images/59271c9195b3dbec8b9d5a5267804f91.jpg) + +### 9. 联系方式 必填* + +* 客服电话:请填写真实有效的客服电话,将在交易记录中向买家展示,提供咨询服务。请确保电话畅通,以便入驻后微信、支付宝平台将回拨确认。 +* 超级管理员证件:证件要求同法人证件。 + * 如超级管理员即法人,则无需二次提供超管证件。 + * 如超级管理员并非法人,则需额外提供《[业务办理授权函](https://kf.qq.com/faq/220509Y3Yvym220509fQvYR7.html)》。 +* 联系电话:用于接收重要管理信息及日常操作验证码。 +* 邮箱:用于接收日常业务邮件。 + +### 10. 结算方式 必填* + +支付宝支持选择结算至**企业**支付宝账户或银行卡(推荐结算至企业支付宝账户)。微信仅支持选择结算至银行卡。结算至银行卡请提供以下信息: + +* 开户银行 +* 开户名称:需与营业主体相同 +* 银行账号 + +## 发送邮件 + +请将材料整理发送邮件至 [PaymentSupport@taptap.com](mailto:PaymentSupport@taptap.com) 申请创建商户,邮件主题请说明您希望开通内购服务的游戏以及支付方式。 + +## 创建支付宝商户 + +支付宝商户支持通过链接自助创建。 + +在材料审核通过后,TapTap 工作人员将通过邮件回复自助创建商户链接,根据指引操作即可完成。 + +## 创建微信支付商户 + +微信支付商户需 TapTap 工作人员协助开通,创建微信支付商户需要完成以下步骤: + +1. TapTap 工作人员录入材料,向微信提交审核。 +2. 提交审核后,超级管理员需扫描二维码(二维码由 TapTap 工作人员通过邮件回复)确认本次提交材料。 +3. 审核通过后,超级管理员需扫描二维码(二维码由 TapTap 工作人员通过邮件回复)完成账户验证及创建商户签约。账户验证支持以下方式二选一: + * 对公转账确认转账金额; + * 法人使用微信号扫一扫。 +4. 完成签约后,超级管理员需在微信中查看微信消息,开通分账能力。邀请开通分账能力的微信消息由 TapTap 工作人员通过系统发送。 + +## 常见问题 + +### Q:游戏没上线可以申请开通微信 App 支付吗? + +游戏上线后才可以申请开通微信 App 支付。 +但请放心,游戏未上线可以开通支付宝支付并完整使用游戏内购服务。游戏上线提交材料创建微信支付商户后,TapTap 工作人员在后台配置即可开通微信支付,您无需更新游戏。 + +### Q:创建支付宝商户时,结算信息提示“开户名称和商户名称不一致,请补充上传授权函”怎么办? + +您填写的「签约支付宝账号」需和营业执照为同一企业主体。 +如果您无法确认填写的签约支付宝账号的主体,您可以使用其他支付宝账号对其转账,转账页面会显示其账号的主体信息。 \ No newline at end of file diff --git a/cn/docs/sdk/tap-iap/develop/_category_.json b/cn/docs/sdk/tap-iap/develop/_category_.json new file mode 100644 index 000000000..3446da847 --- /dev/null +++ b/cn/docs/sdk/tap-iap/develop/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "开发指南", + "collapsed": true, + "position": 2 +} diff --git a/cn/docs/sdk/tap-iap/develop/android.mdx b/cn/docs/sdk/tap-iap/develop/android.mdx new file mode 100644 index 000000000..1792b6560 --- /dev/null +++ b/cn/docs/sdk/tap-iap/develop/android.mdx @@ -0,0 +1,218 @@ +--- +title: Android 集成指南 +sidebar_position: 2 +--- + +import MultiLang from "/src/docComponents/MultiLang"; +import { Conditional } from "/src/docComponents/conditional"; +import v4SDKVersions from '/src/docComponents/v4SDKVersions'; +import CodeBlock from '@theme/CodeBlock'; + + +## **环境要求** + +- Gradle 版本不低于 6.1.1,Android AGP 插件版本不低于 4.0.1; + +## **准备** + +- 参照 [准备工作](/sdk/access/get-ready/) 所述创建 app,配置 app 参数并且绑定 API 域名 +- 参照 [TapSDK 快速开始](/sdk/access/quickstart/) 配置包名和签名 + +## **SDK 指南** + +### **SDK 集成** + +打开项目的 `project/app/build.gradle` 文件,添加 gradle 配置如下: + + + {`dependencies { + ... + // TapTapIAP dependency + implementation 'com.taptap.android.payment:iap:${v4SDKVersions.taptap.android}' + implementation 'com.taptap.android.payment:base:${v4SDKVersions.taptap.android}' + implementation 'com.taptap.android.payment:wechat:${v4SDKVersions.taptap.android}' + implementation 'com.taptap.android.payment:alipaycn:${v4SDKVersions.taptap.android}' +} +`} + + + + + +### **SDK 初始化** + +添加 TapTapIAP 的依赖项后,您需要初始化 `TapTapIAP` 实例。`TapTapIAP` 是 SDK 与应用的其余部分之间进行通信。`TapTapIAP` 为许多常见的操作提供了方法。 + +首先 在应用启动时需要进行 SDK 初始化, 通过设置 `TapTapSdkOptions`, 完成 SDK 初始化, `TapTapSdk.init` 。 + +这里需要设置对应在开发者后台申请的 `ClientID` 和 `ClientToken`,用于校验是否有权限使用 `TapTapIAP`。 + +```java +TapTapSdkOptions sdkOptions = new TapTapSdkOptions( + "应用的 ClientID", // clientId 开发者平台申请 + "应用的 ClientToken", // clientToken 开发者平台申请 + TapTapRegion.GLOBAL, // 地区 + "", // 分包渠道名称 + "", // 游戏版本, 为空为null会取AppVersion + false, // 是否自动上报GooglePlay内购支付成功事件 仅 [TapTapRegion.GLOBAL] 生效 + false, // 自定义字段是否能覆盖内置字段 + null, // 自定义属性,启动首个预置事件(device_login)会带上这些属性 + null, // OAID证书, 用于上报 OAID 仅 [TapTapRegion.CN] 生效 + false // 是否开启 log,建议 Debug 开启,Release 关闭 +); + +TapTapSdk.init(context, sdkOptions); +``` + +创建 `TapTapIAP`,请使用 `newBuilder()`这里会根据SDK.init所设置的 `ClientID` 和 `ClientToken`校验是否有权限使用 `TapTapIAP`。 + +```java +// 创建 TapTapIAP 实例 + TapTapIAP tapTapIAP = TapTapIAP.newBuilder().build(); + ``` + +### **展示可供购买的商品** + +初始化完成 `TapTapIAP` 后,您就可以查询可售的商品并将其展示给用户了。 + +查询应用内商品详情,请调用 `queryProductDetailsAsync()`。为了处理该异步操作的结果,您还必须指定实现 ` ProductDetailsResponseListener` 接口的监听器。然后,您可以替换 `onProductDetailsResponse()`,该方法会在查询完成时通知监听器,如以下示例所示: + +```java +List queryProductList = new ArrayList<>(); +// 支持批量查询 Product, 设置好对应的 ProductID、ProductType +// ProductType 目前仅支持 ProductType.INAPP +for (int i = 0; i < products.length; i++) { + + String productId = products[i]; + Product product = Product.newBuilder() + .setProductId(productId) + .setProductType(ProductType.INAPP) + .build(); + queryProductList.add(product); +} +QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder() + .setProductList(queryProductList).build(); +tapTapIAP.queryProductDetailsAsync(params, new ProductDetailsResponseListener() { + @Override + public void onProductDetailsResponse(TapPaymentResult result, + List productDetails, List unavailableProductIds) { + ... + + // check TapPaymentResult + // process returned productDetails + } +}); +``` + +### **启动购买流程** + +如需从应用发起购买请求,请从应用的主线程调用 `launchBillingFlow()` 方法。此方法接受对 `BillingFlowParams` 对象的引用,该对象包含通过调用 `queryProductDetailsAsync()` 获取的相关 `ProductDetails` 对象。如需创建 `BillingFlowParams` 对象,请使用 `BillingFlowParams.Builder` 类。 + +```java +// An activity reference from which the billing flow will be launched. +Activity activity = ...; +ProductDetailsParams productDetailsParams = + ProductDetailsParams.newBuilder() + // retrieve a value for "productDetails" by calling queryProductDetailsAsync() + .setProductDetails(productDetails) + .build(); + +BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder() + .setProductDetailsParams(productDetailsParams) + .setObfuscatedAccountId("xxx") //Specifies an optional obfuscated string that is uniquely associated with the order(or another information) in your app. + .build(); +// Launch the billing flow +TapPaymentResult result = tapTapIAP.launchBillingFlow(activity, + billingFlowParams, + new PurchaseUpdatedListener() { + @Override + public void onPurchaseUpdated(TapPaymentResult result, Purchase purchases) { + // To be implemented in a later section. + } + } +); +``` + +`launchBillingFlow()` 方法会返回 `TapPaymentResponseCode` 中列出的几个响应代码之一。请务必检查此结果,以确保在启动购买流程时没有错误。`TapPaymentResponseCode` 为 `OK` 表示成功启动。成功调用 `launchBillingFlow()` 后,会向用户展示收银台。 + +### **购买流程中订单状态监听** + +在购买流程中, `TapTapIAP` 会调用 `onPurchasesUpdated()`,以将购买的订单状态变更实时传给实现 PurchasesUpdatedListener 接口的监听器。您可以在初始化时使用 `setListener()` 方法指定监听器。您必须实现 `onPurchasesUpdated()` 来处理可能的响应代码。以下提供了一个 `onPurchasesUpdated()` 示例 : + +```java +@Override +public void onPurchaseUpdated(TapPaymentResult result, Purchase purchase) { + if (result.getResponseCode() == TapPaymentResponseCode.OK + && purchases != null) { + handlePurchase(purchase); + } else if (result.getResponseCode() == TapPaymentResponseCode.USER_CANCELED) { + // Handle an error caused by a user cancelling the purchase flow. + } else { + // Handle any other error codes. + } +} +``` +### **进行商品的发放,且完成订单** + +在用户进行完了任意商品的购买, 请确认为该用户发放对应的商品或者解锁对应的关卡。在确定商品发放之后需要调用 `finishPurchaseAsync` 告知 `TapTapIAP` 已完成商品的发放。以下是个代码实例: + +```java +Purchase purchase = ...; +FinishPurchaseParams params = FinishPurchaseParams.newBuilder() + .setId(purchase.getOrderId()) // Required + .setOrderToken(purchase.getOrderToken()) // Required + .setPurchaseToken(purchase.getPurchaseToken()) // Required + .build(); +tapTapIAP.finishPurchaseAsync(params, new FinishPurchaseResponseListener() { + @Override + public void onFinishPurchaseResponse(TapPaymentResult result, Purchase purchase) { + } +}); +``` +:::tip +确认发放商品非常重要, 如果您没有调用 `finishPurchaseAsync` 来完成订单, 用户将无法再次购买该商品,且该订单将会在 3天后自动退款。 +::: + +### **获取未完成的订单列表** + +使用 `PurchasesUpdatedListener` 监听购买交易变更,无法完全确保您的应用会处理所有购买交易。有时您的应用可能不知道用户进行了部分购买交易。在下面这几种情况下,您的应用可能会跟踪不到或不知道购买交易的发生: + +- **在购买过程中出现网络问题**:用户成功购买了商品并收到了对应渠道的确认消息,但用户设备在通过 `PurchasesUpdatedListener` 收到购买交易的通知之前失去了网络连接。 +- **多部设备**:用户在一部设备上购买了一件商品,然后在切换设备时期望看到该商品。 +- **异常崩溃**:用户在外部购买成功时,应用出现了崩溃的情况。 + +为了处理这些情况,请确保您的应用在 `onResume()` 方法中调用 `tapTapIAP.queryUnfinishedPurchaseAsync()`,以确保所有购买交易都得到正确处理。 + +以下示例展示了如何提取用户的未完成订单列表: + +```java +tapTapIAP.queryUnfinishedPurchaseAsync(new PurchasesResponseListener() { + @Override + public void onQueryPurchasesResponse(TapPaymentResult result, List purchases) { + if (purchases != null) { + // Process Purchases. + ... + ... + } + } + }); +``` + +### **处理 TapPaymentResult 响应代码** + +当使用 `TapTapIAP` 结算库调用触发操作时,该库会返回 `TapPaymentResult` 响应,并将结果告知开发者。例如,如果您使用 `queryProductDetailsAsync` 且返回 `OK` ,并提供正确的 `ProductDetails` 对象;或者返回了其他类型,代表了无法提供 `ProductDetails` 对象的原因。 + +并非所有类型都是错误。下面列举了一些 `TapPaymentResponseCode` 不是错误的: + +- `TapPaymentResponseCode.OK`:代表业务已成功执行。 +- `TapPaymentResponseCode.USER_CANCELED`:代表用户没有完成流程,就离开了页面 + +其他的一些错误类型可以用于调试和上报使用: + +| **可重试的 CODE** | **问题** | **可以尝试的解决方案** | +| ------------------ | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| NETWORK_ERROR | 此错误代表设备和 TapTapIAP 系统之间的网络连接出现问题 | 可以使用简单的重试策略或指数退避算法 | +| ITEM_ALREADY_OWNED | 这个类型表明用户已经购买过 非消耗品 商品, 再次购买时会返回该错误 | 为了避免出现这个问题, 在展示商品界面就提示用户已经购买该商品, 且无法点进进行购买流程. | +| USER_CANCELED | 用户已退出结算流程 | | +| ITEM_UNAVAILABLE | 商品无效, 有可能是商品已经过期或者商品已经被下架 | 确保您通过 `queryProductDetailsAsync` 刷新商品详情。如果商品无效则不在界面上展示给用户 | +| DEVELOPER_ERROR | 这是一个严重错误,表明您未正确使用 API。例如,向 launchBillingFlow 提供不正确的参数可能会导致此错误 | | diff --git a/cn/docs/sdk/tap-iap/develop/sandbox.mdx b/cn/docs/sdk/tap-iap/develop/sandbox.mdx new file mode 100644 index 000000000..cd50341ec --- /dev/null +++ b/cn/docs/sdk/tap-iap/develop/sandbox.mdx @@ -0,0 +1,12 @@ +--- +title: 沙盒环境 +sidebar_position: 4 +--- + +TapTap IAP 沙盒环境进行支付测试, 如果使用沙盒账号发起 IAP, 那么对应所有渠道将按照最低标准进行支付, 以便开发者进行支付测试。 + +## 沙盒账号设置 +进入开发者中心后台, 按照如下步骤设置和添加对应沙盒账号: + +![](https://img.tapimg.com/market/images/7309efbf7c1971d44c73a53494193bb4.png) + diff --git a/cn/docs/sdk/tap-iap/develop/server.mdx b/cn/docs/sdk/tap-iap/develop/server.mdx new file mode 100644 index 000000000..efb9470d1 --- /dev/null +++ b/cn/docs/sdk/tap-iap/develop/server.mdx @@ -0,0 +1,686 @@ +--- +title: 服务端开发指南 +sidebar_position: 3 +--- + +import MultiLang from "/src/docComponents/MultiLang"; + +:::tip +服务通用规则请参考 [请求规则和签算](/sdk/tap-iap/develop/server/#请求规则和签算) +::: + +## 主查服务 + +#### 请求域名: +- `https://cloud-payment.tapapis.com` + + +| ** 接口地址 ** | **Method** | **描述** | +| ----------------------------- | ---------- | -------------------- | +| `/order/v1/info?client_id={{client_id}}&order_id={{order_id}}` | `GET` | 查询订单信息 | +| `/order/v1/unconfirmed?client_id={{client_id}}` | `GET` | 查询未核销的订单列表 | +| `/order/v1/verify?client_id={{client_id}}` | `POST` | 核销订单 | + +### 查询订单信息 + +#### 服务 URL +- https://{{domain}}/order/v1/info?client_id={{client_id}}&order_id={{order_id}} + +#### 请求方式 +- GET + +#### 请求签算 +- 详见 [请求规则和签算](/sdk/tap-iap/develop/server/#请求规则和签算) + +通过订单 ID 查询订单详细信息和支付状态 + +``` +curl -X GET \ + -H 'X-Tap-Sign: {{signature}}' \ + -H 'X-Tap-Ts: {{unix timestamp}}' \ + -H 'X-Tap-Nonce: {{random nonce}}' \ + https://{{domain}}/order/v1/info?client_id={{client_id}}&order_id={{order_id}} +``` + +`data.order` 对象结构见 [订单信息](/sdk/tap-iap/develop/server/#订单信息) + +``` +{ + "data": { + "order": {} + }, + "success": true +} +``` + +### 查询未核销的订单列表 + +#### 服务 URL +- https://{{domain}}/order/v1/unconfirmed?client_id={{client_id}} + +#### 请求方式 +- GET + +#### 请求签算 +- 详见 [请求规则和签算](/sdk/tap-iap/develop/server/#请求规则和签算) + +查询当前未核销的订单列表,正常情况下在用户支付成功后,应通过 verify 接口核销订单并同时保证对用户发货成功。如果因异常原因没有完成核销,可以通过此接口查询,重新 verify 并完成发货。 + +``` +curl -X GET \ + -H 'X-Tap-Sign: {{signature}}' \ + -H 'X-Tap-Ts: {{unix timestamp}}' \ + -H 'X-Tap-Nonce: {{random nonce}}' \ + https://{{domain}}/order/v1/unconfirmed?client_id={{client_id}} +``` + +`data.list` 数组内对象结构见 [订单信息](/sdk/tap-iap/develop/server/#订单信息) + +``` +{ + "data": { + "list": [ + {} + ] + }, + "success": true +} +``` + +### 核销订单 + +#### 服务 URL +- https://{{domain}}/order/v1/verify?client_id={{client_id}} + +#### 请求方式 +- POST[application/json; charset=utf-8] + +#### 请求正文信息 + +| ** 参数名称 ** | ** 必填 ** | ** 格式 ** | ** 描述 ** | +| --------------| --------------| --------------| --------------| +| `order_id` | Y | string | 订单唯一 ID | +| `purchase_token` | Y | string | 用于订单核销的 token | + +#### 请求签算 +- 详见 [请求规则和签算](/sdk/tap-iap/develop/server/#请求规则和签算) + +当支付成功后,核销订单表示已经确认支付结果并已对买家完成发货,订单状态也会从 `charge.succeeded` 变为 `charge.confirmed` + +``` +curl -X POST \ + -H 'X-Tap-Sign: {{signature}}' \ + -H 'X-Tap-Ts: {{unix timestamp}}' \ + -H 'X-Tap-Nonce: {{random nonce}}' \ + -H 'Content-Type: application/json; charset=utf-8' + -d '{"order_id":"{{order_id}}","purchase_token":"{{purchase_token}}"}' + https://{{domain}}/order/v1/verify?client_id={{client_id}} +``` + +`data.order` 对象结构见 [订单信息](/sdk/tap-iap/develop/server/#订单信息) + +``` +{ + "data": { + "order": {} + }, + "success": true +} +``` + +## Webhook 回调 + +:::tip +同样的通知可能会多次发送,商户系统必须正确处理重复通知。 + +推荐做法是:当商户系统收到通知时,首先进行签名验证,然后检查对应业务数据的状态,如未处理,则进行处理;如已处理,则直接返回成功。 + +在处理业务数据时建议采用数据锁进行并发控制,以避免可能出现的数据异常。 +::: + +### Webhook 说明 + +目前 Webhook 支持监听「充值成功」「退款成功」「退款失败」事件。对于「充值成功」建议采取主动核销订单,根据订单状态完成发货。 + +1. 需要依次进入 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > TapPayment > 商品与订单 > API 密钥**,检查是否有已生效的密钥,没有则需要 **添加新的密钥** +2. 需要依次进入 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > TapPayment > 商品与订单 > Webhooks 设置 > 添加**,添加有效的 **充值成功** URL。 + +### Webhook 请求 + +#### 服务 URL +- 由开发者提供,在 Webhooks 设置中添加 + +#### 请求方式 +- POST[application/json; charset=utf-8] + +#### 请求正文信息 + +| ** 参数名称 ** | ** 必填 ** | ** 格式 ** | ** 描述 ** | +| --------------| --------------| --------------| --------------| +| `order` | Y | object | 对象结构见 [订单信息](/sdk/tap-iap/develop/server/#订单信息) | +| `event_type` | Y | string | 事件枚举见 [Webhook的事件枚举](/sdk/tap-iap/develop/server/#webhook的事件枚举) | + +#### 请求签算 +- 详见 [请求规则和签算](/sdk/tap-iap/develop/server/#请求规则和签算) + +``` +curl -X POST \ + -H 'X-Tap-Sign: {{signature}}' \ + -H 'X-Tap-Ts: {{unix timestamp}}' \ + -H 'X-Tap-Nonce: {{random nonce}}' \ + -H 'Content-Type: application/json; charset=utf-8' + -d '{"order":{},"event_type":"charge.succeeded"}' + {{your webhook url}} + +``` + +### Webhook 响应 + +``` +{ + "code": "SUCCESS", + "msg": "" +} +``` + +#### 字段描述 + +| **字段** | **类型** | **是否必须** | **描述** | +| -------- | -------- | ------------ | ---------------------------------------- | +| code | string | Y | 状态码,`SUCCESS` 为接收成功,`FAIL` 或其他均认为失败 | +| msg | string | N | 当接收失败时需返回失败原因 | + +## 请求规则和签算 + +### 请求 Headers + +| **Header** | **是否必须** | **描述** | +| ----------- | ------------ | ------------------------------------------------------------ | +| `X-Tap-Sign` | Y | 接口签算,详见 [签算](/sdk/tap-iap/develop/server/#签算) | +| `X-Tap-Ts` | Y | 请求方当前时间 unix timestamp | +| `X-Tap-Nonce` | Y | 随机数,需要大于等于 6 字节,小于等于 60 字节,每次请求需重新生成 | + +### 请求 Request + +#### 保留参数 + +所有 HTTP METHOD 必传,需要作为查询参数的一部分 + +| **Key** | **描述** | +| --------- | --------------- | +| client_id | 开发平台应用 ID | + +``` +https://{{domain}}/order/v1/info?client_id={{client_id}}&order_id={{order_id}} +``` + +#### 当请求方法是 POST + +HTTP body 需使用 JSON 编码格式传输参数,即请求 headers 中应该携带 `Content-Type: application/json; charset=utf-8` + +``` +curl -X POST \ + -H 'X-Tap-Sign: {{signature}}' \ + -H 'X-Tap-Ts: {{unix timestamp}}' \ + -H 'X-Tap-Nonce: {{random nonce}}' \ + -H 'Content-Type: application/json; charset=utf-8' + -d '{"order_id":{{order_id}},"purchase_token":"{{purchase_token}}"}' + https://{{domain}}/order/v1/verify?client_id={{client_id}} +``` + +### 请求 Response + +#### 请求成功响应 + +``` +{ + "data": {}, + "now": 1640966400, + "success": true +} +``` + +#### 请求异常响应 + +``` +{ + "data": { + "code": 100004, + "msg": "NotFound: Unknown Error", + "error_description": "order not found" + }, + "now": 1640966400, + "success": false +} +``` + +#### 字段描述 + +| **字段** | **类型** | **是否必须** | **描述** | +| -------- | -------- | ------------ | --------------------------- | +| data | object | Y | 业务数据,或异常信息描述 | +| now | int | Y | 服务端时间 (unix timestamp) | +| success | bool | Y | 响应状态,`true` 为成功 | + +异常响应 `data` 字段描述 + +| **字段** | **类型** | **是否必须** | **描述** | +| ----------------- | -------- | ------------ |------------------------------------------------------------| +| code | int | Y | [错误码](/sdk/tap-iap/develop/server/#错误码) | +| msg | string | Y | 通用错误描述 | +| error_description | string | Y | 详细错误描述,辅助理解和解决发生的错误 | + + +### 签算 + +签算采用 HMAC-SHA256 算法 + +#### 密钥获取 +- 可在 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > TapPayment > 商品与订单 > API 密钥** 查看。 + +#### 签算说明 + +sign = HMAC.New(Sha256, "{{Server Secret}}").Hash(message) + +以下为 `message` 组成 + +``` +{method}\n +{url_path_and_query}\n +{headers}\n +{body}\n +``` + +**method:** 请求 HTTP 方法,如 GET、POST。 + +**url_path_and_query:** 完整请求路径及参数,如 /service/v1/method?client_id={{client_id}}&foo={{foo}}&bar={{bar}}&foo_bar={{foo_bar}} + +**headers:** 所有 `X-Tap-` 前缀的 headers 组合,将 keys 按 ASCII-code order 排序后以换行符 \n 为分隔符拼接。如 {key1}:{value1}\n{key2}:{value2}\n{key3}:{value3}。**为避免不同网络框架导致 keys 的排序结果不一致,签算时需要执行 key 转小写操作 key = tolower(key)** + +**body:** 请求体,如果请求体为空,则最后一行仅为一个 \n + +以下为请求体为空时的 `message` 组成 + +``` +{method}\n +{url_path_and_query}\n +{headers}\n +\n +``` + +:::tip +请求体 body,无需处理请求参数的顺序。建议用 String 接收 Webhook 请求的 RequestBody,验证请求签算后,再完成数据的反序列化 +::: + + + + +<> + +```java +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpClient; +import java.net.http.HttpResponse; +import java.util.*; +import java.util.stream.Collectors; +public class SignatureExample { + + public static String signRequest(String method, URI uri, String body, Map> headers, String secret) throws Exception { + String urlPathAndQueryPart = uri.getRawPath() + (uri.getRawQuery() != null ? "?" + uri.getRawQuery() : ""); + String headersPart = getHeadersPart(headers); + + // 包含请求体的签名字符串部分 + String signParts = method.toUpperCase() + "\n" + urlPathAndQueryPart + "\n" + headersPart + "\n" + body + "\n"; + + System.out.println("Sign Parts:\n" + signParts); + + Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256"); + sha256_HMAC.init(secretKey); + + byte[] hash = sha256_HMAC.doFinal(signParts.getBytes()); + return Base64.getEncoder().encodeToString(hash); + } + + private static String getHeadersPart(Map> headers) throws Exception { + TreeMap sortedHeaders = new TreeMap<>(); + headers.forEach((key, value) -> { + String lowerKey = key.toLowerCase(); + if (lowerKey.equals("x-tap-sign")) { + return; + } + if (lowerKey.startsWith("x-tap-")) { + if (value.size() > 1) { + throw new RuntimeException("Invalid header, " + lowerKey + " has multiple values"); + } + sortedHeaders.put(lowerKey, value.get(0)); + } + }); + + return sortedHeaders.entrySet().stream() + .map(entry -> entry.getKey() + ":" + entry.getValue()) + .collect(Collectors.joining("\n")); + } + + public static void main(String[] args) { + try { + String secret = "VRy8aS2xbwImQUwtxc6vs4v51DaJWdlO"; + String body = "{\"event_type\":\"charge.succeeded\",\"order\":{\"order_id\":\"1790288650833465345\",\"purchase_token\":\"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=\",\"client_id\":\"o6nD4iNavjQj75zPQk\",\"open_id\":\"4+Axcl2RFgXbt6MZwdh++w==\",\"user_region\":\"US\",\"goods_open_id\":\"com.goods.open_id\",\"goods_name\":\"TestGoodsName\",\"status\":\"charge.succeeded\",\"amount\":\"19000000000\",\"currency\":\"USD\",\"create_time\":\"1716168000\",\"pay_time\":\"1716168000\",\"extra\":\"1111111111111111111\"}}"; + URI uri = new URI("https://example.com/my-service/v1/my-method"); + + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .uri(uri) + .header("Content-Type", "Content-Type: application/json; charset=utf-8") + .header("X-Tap-Ts", "1716168000") + .header("X-Tap-Nonce", "V7v7zJ"); + + // Considering body to be added for a POST request + HttpRequest request = requestBuilder + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + String method = "POST"; // Since we are using the POST method in this example + String signature = signRequest(method, uri, body, request.headers().map(), secret); + System.out.println("Signature: " + signature); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + + +<> + +```go +package main + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "io" + "net/http" + "sort" + "strings" +) + +func main() { + //nolint:gosec + secret := "VRy8aS2xbwImQUwtxc6vs4v51DaJWdlO" + body := []byte(`{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345","purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk","open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id","goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD","create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}`) + url := "https://example.com/my-service/v1/my-method" + method := "POST" + header := http.Header{ + "Content-Type": {"Content-Type: application/json; charset=utf-8"}, + "X-Tap-Ts": {"1716168000"}, + "X-Tap-Nonce": {"V7v7zJ"}, + } + ctx := context.Background() + req, _ := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(body)) + req.Header = header + sign, err := Sign(req, secret) + if err != nil { + panic(err) + } + req.Header.Set("X-Tap-Sign", sign) + fmt.Println(sign) +} + +// Sign signs the request. +func Sign(req *http.Request, secret string) (string, error) { + methodPart := req.Method + urlPathAndQueryPart := req.URL.RequestURI() + headersPart, err := getHeadersPart(req.Header) + if err != nil { + return "", err + } + bodyPart, err := io.ReadAll(req.Body) + if err != nil { + return "", err + } + signParts := methodPart + "\n" + urlPathAndQueryPart + "\n" + headersPart + "\n" + string(bodyPart) + "\n" + fmt.Println(signParts) + h := hmac.New(sha256.New, []byte(secret)) + h.Write([]byte(signParts)) + rawSign := h.Sum(nil) + sign := base64.StdEncoding.EncodeToString(rawSign) + return sign, nil +} + +// getHeadersPart returns the headers part of the request. +func getHeadersPart(header http.Header) (string, error) { + var headerKeys []string + for k, v := range header { + k = strings.ToLower(k) + if !strings.HasPrefix(k, "x-tap-") { + continue + } + if k == "x-tap-sign" { + continue + } + if len(v) > 1 { + return "", fmt.Errorf("invalid header, %q has multiple values", k) + } + headerKeys = append(headerKeys, k) + } + sort.Strings(headerKeys) + headers := make([]string, 0, len(headerKeys)) + for _, k := range headerKeys { + headers = append(headers, fmt.Sprintf("%s:%s", k, header.Get(k))) + } + return strings.Join(headers, "\n"), nil +} +``` + + + +<> + +```python +import base64 +import hashlib +import hmac +from typing import Dict, List +from urllib.parse import urlparse + +def sign_request(method: str, url: str, body: str, headers: Dict[str, List[str]], secret: str) -> str: + # 提取URL路径和查询字符串部分 + parsed_url = urlparse(url) + url_path_and_query = parsed_url.path + ('?' + parsed_url.query if parsed_url.query else '') + + # 获取符合条件的头部信息并排序 + headers_part = get_headers_part(headers) + + # 拼接签名字符串 + sign_parts = f"{method}\n{url_path_and_query}\n{headers_part}\n{body}\n" + + print("Sign Parts:\n", sign_parts) + + # 使用HMAC SHA256算法生成签名 + raw_sign = hmac.new(secret.encode(), sign_parts.encode(), hashlib.sha256).digest() + + # 对签名进行Base64编码 + sign = base64.b64encode(raw_sign).decode() + + return sign + +def get_headers_part(headers: Dict[str, List[str]]) -> str: + # 筛选和排序头部 + headers = {k.lower(): v for k, v in headers.items() if k.lower().startswith('x-tap-') and k.lower() != "x-tap-sign"} + header_keys = sorted(headers.keys()) + + # 组装头部字符串 + headers_str = '\n'.join(f"{k}:{headers[k][0]}" for k in header_keys if len(headers[k]) == 1) + + if any(len(headers[k]) > 1 for k in header_keys): + raise ValueError("Invalid header: has multiple values") + + return headers_str + +# 示例使用 +secret = "VRy8aS2xbwImQUwtxc6vs4v51DaJWdlO" +url = "https://example.com/my-service/v1/my-method" +method = "POST" +body = '{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345",' \ + '"purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk",' \ + '"open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id",' \ + '"goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD",' \ + '"create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}' +headers = { + "Content-Type": ["Content-Type: application/json; charset=utf-8"], + "X-Tap-Ts": ["1716168000"], + "X-Tap-Nonce": ["V7v7zJ"], +} + +try: + sign = sign_request(method, url, body, headers, secret) + print("Signature: ", sign) +except Exception as e: + print("Error: ", str(e)) + +``` + + + +<> + +```php + $value) { + $key = strtolower($key); + if (count($value) > 1) { + throw new Exception("Multiple values for header: " . $key); + } + if ($key === "x-tap-sign") { + continue; + } + if (strpos($key, 'x-tap-') === 0) { + $signHeaders[$key] = $value; // Assuming each header has a single value + } + } + $headerKeys = []; + foreach ($signHeaders as $key => $value) { + if (!(strpos($key, 'x-tap-') === 0)) { + continue; + } + $headerKeys[] = $key; + } + sort($headerKeys); + $headerParts = []; + foreach ($headerKeys as $key) { + $headerParts[] = $key . ':' . $signHeaders[$key][0]; + } + return implode("\n", $headerParts); +} + +// 示例使用 +$secret = "VRy8aS2xbwImQUwtxc6vs4v51DaJWdlO"; +$url = "https://example.com/my-service/v1/my-method"; +$method = "POST"; +$body = '{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345","purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk","open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id","goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD","create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}'; +$headers = [ + "Content-Type" => ["Content-Type: application/json; charset=utf-8"], + "X-Tap-Ts" => ["1716168000"], + "X-Tap-Nonce" => ["V7v7zJ"] +]; +try { + $sign = signRequest($method, $url, $body, $headers, $secret); + echo "Signature: " . $sign . "\n"; +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; +} + +``` + + + + + +上述示例签算结果 + +``` +# 参与签算部分 +POST\n +/my-service/v1/my-method\n +x-tap-nonce:V7v7zJ\n +x-tap-ts:1716168000\n +{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345","purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk","open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id","goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD","create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}\n + +# 签算结果 (X-Tap-Sign) +PyKQzlI65e0I9noVxcQc7FPU3nEyEFHKfRde65F6vhI= +``` + +## 通用对象结构描述 + +### 订单信息 + +| **参数** | **类型** | **是否必须** | **描述** | +| -------------- | -------- | ------------ | ------------------------------------------------------------ | +| order_id | string | Y | 订单唯一 ID | +| purchase_token | string | Y | 用于订单核销的 token | +| client_id | string | Y | 应用的 Client ID | +| open_id | string | Y | 用户的开放平台 ID | +| user_region | string | Y | [用户地区] | +| goods_open_id | string | Y | 商品唯一 ID | +| goods_name | string | Y | 商品名称 | +| status | string | Y | [订单状态](/sdk/tap-iap/develop/server/#订单状态) | +| amount | string | Y | 金额(本币金额 x 1,000,000) | +| currency | string | Y | [币种] | +| create_time | string | Y | 创建时间 | +| pay_time | string | Y | 支付时间 | +| extra | string | Y | 商户自定义数据,如角色信息等,长度不超过 255 UTF-8 字符 | + +#### 订单状态 + +| **订单状态** | **描述** | +| ---------------- | ------------ | +| `charge.pending` | 待支付 | +| `charge.succeeded` | 支付成功 | +| `charge.confirmed` | 已核销 | +| `charge.overdue` | 支付超时关闭 | +| `refund.pending` | 退款中 | +| `refund.succeeded` | 退款成功 | +| `refund.failed` | 退款失败 | +| `refund.rejected` | 退款被拒绝 | + +### Webhook的事件枚举 + +| **event_type** | **描述** | +| ---------------- | -------- | +| `charge.succeeded` | 充值成功 | +| `refund.succeeded` | 退款成功 | +| `refund.failed` | 退款失败 | + +## 错误码 + +| **code** | **描述** | +| -------- | ------------ | +| `-1` | 非法请求 | +| `100000` | 支付服务异常 | +| `100004` | 订单不存在 | +| `100018` | 订单验证错误 | diff --git a/cn/docs/sdk/tap-iap/develop/unity.mdx b/cn/docs/sdk/tap-iap/develop/unity.mdx new file mode 100644 index 000000000..120b92ce2 --- /dev/null +++ b/cn/docs/sdk/tap-iap/develop/unity.mdx @@ -0,0 +1,249 @@ +--- +title: Unity 集成指南 +sidebar_position: 1 +--- + + +import MultiLang from "/src/docComponents/MultiLang"; +import v4SDKVersions from '/src/docComponents/v4SDKVersions'; +import CodeBlock from '@theme/CodeBlock'; + + +## 环境要求 + +- 支持 Unity 2019.4 及以上版本 + +## 准备 + +- 参照 [准备工作](/sdk/access/get-ready/) 所述创建 app,配置 app 参数并且绑定 API 域名 +- 参照 [TapSDK 快速开始](/sdk/access/quickstart/) 配置包名和签名 + +## 获取 SDK + +:::tip +Unity 2020.3.15 之前的版本为了避免后面构建报错建议升级 Gradle 版本,可参考该 [Gradle 升级步骤文档](/sdk/tap-iap/faq/#1unity-2020315-之前的版本升级-gradle-版本的操作步骤)。 +::: + +以下介绍了**通过 Unity Package Manager 导入**或者**修改 Packages/manifest.json 配置文件引入**两种方式,根据项目需要,任选其一即可: + +### 1、通过 Package Manager 可视化界面引入 + +因为 TapTap IAP 依赖 **EDM4U(External Dependency Manager for Unity)** 来处理 Android 相关的依赖,因此引入依赖的步骤是**先安装 EDM4U 库,然后安装 TapTap IAP**。 + +#### 安装 EDM4U + +下面介绍了两种方式安装 EDM4U,分别是通过 **OpenUPM 安装**以及**手动下载安装**; + +- **方法一:通过 OpenUPM 安装 EDM4U** + +EDM4U 可以通过 OpenUPM 进行下载,开发者可以通过 **Edit > Project Settings > Package Manager** 来注册使用 OpenUPM。 + +![](https://capacity-files.lcfile.com/gpS8BcTVJIdSpMxyDipIWORX4Gb2filA/NdoQbmRRvovet4x3LGUcsTXWnjb.png) + +- **方法二:手动下载安装** + +开发者可以通过 [Google APIs for Unity](https://developers.google.com/unity/archive#external_dependency_manager_for_unity) 下载 UPM 安装包. 并且通过[从本地磁盘安装](https://docs.unity3d.com/Manual/upm-ui-local.html)的方式进行安装。 + +#### 安装 TapTap IAP + +TapTap IAP 可以通过 NPMJS 进行安装, 开发者可以通过 **Edit > Project Settings > Package Manager** 来注册使用 NPMJS。 + +![](https://capacity-files.lcfile.com/sJDhXK4vAwAYX7BpQFh1SrQzeUmXk165/taptap.png) + +<> + 配置完成以后就可以在 Window > Package Manager > My Registries 中安装 TapTapSDK IAP {v4SDKVersions.taptap.unity}。 + 如果安装目录中没有 TapTapSDK IAP, 请尝试在 Edit > Project Settings > Package Manager 中重新注册 NPMJS。 + + +![](https://img.tapimg.com/market/images/cda0911e47723fba9bee9a29ef62260f.png) + + +### 2、修改 Packages/manifest.json 文件 + + + {` "dependencies": { + "com.taptap.sdk.iap": "${v4SDKVersions.taptap.unity}", //添加 TapTap IAP + "com.unity.purchasing": "3.1.0", //TapTap IAP 所须依赖 + "com.google.external-dependency-manager": "1.2.179", //TapTap IAP 所须依赖 + "com.taptap.sdk.core": "${v4SDKVersions.taptap.unity}", + ... + ... + }, + + // 添加 Registries + "scopedRegistries": [ + { + "name": "taptap", + "url": "https://registry.npmjs.org", + "scopes": ["com.tapsdk", "com.taptap", "com.leancloud"] + }, + { + "name": "openupm", + "url": "https://package.openupm.com", + "scopes": [ + "com.google" + ] + } + ]`} + + + +:::tip + +**处理安卓依赖失败** + +EDM4U 会自动监控 TapTap IAP 引入的 Android 依赖,但是在某些特殊情况下自动依赖解析可能失败,开发者可以通过 **Assets > External Dependency Manager > Android Resolver > Force Resolve** 来强制依赖解析。在测试之前请确认对于 `com.taptap.android.payment:unity` 的依赖被加到了 mainTemplate.gradle 文件中,如果 EDM4U 没有自动添加这个依赖,开发者也可以通过手动添加的方式处理依赖。 +::: + +## SDK 指南 + +我们在 [Unity IAP](https://unity.com/products/iap) 的框架下实现了一个新的商店实现,通过这种方式降低已经有 Google Play 或者 App Store 内购的 app 的接入成本。如果开发者是第一次接触到内购的流程,我们会在接下来简要说明 Unity 的内购流程,开发者也可以在 Unity 的[官方文档](https://docs.unity3d.com/Packages/com.unity.purchasing@4.8/manual/Overview.html) 中了解有关内购的详细内容。 + +### 初始化 Unity Gaming Services + +调用 `UnityServices.InitializeAsync()` 来一次性初始化 Unity Gaming Services。 这个方法会返回一个 `Task` 对象,开发者可以通过这个对象来获取初始化的状态信息。 + +```cs +using System; +using Unity.Services.Core; +using Unity.Services.Core.Environments; +using UnityEngine; + +public class InitializeUnityServices : MonoBehaviour +{ + public string environment = "production"; + + async void Start() + { + try + { + var options = new InitializationOptions() + .SetEnvironmentName(environment); + + await UnityServices.InitializeAsync(options); + } + catch (Exception exception) + { + // An error occurred during services initialization. + } + } +} +``` + +### 初始化 IAP + +确保你在初始化 IAP 的时候添加 `TapPurchasingModule`,并且设置正确的 `clientId` 和 `clientToken`。 + +```cs +using UnityEngine; +using UnityEngine.Purchasing; +using UnityEngine.Purchasing.Extension; +using TapSDK.IAP; + +public class MyIAPManager : IStoreListener { + + private IStoreController controller; + private IExtensionProvider extensions; + + public MyIAPManager () { + var builder = ConfigurationBuilder.Instance(TapPurchasingModule.Instance); + builder.Configure().SetClientId("Your Client ID Here"); + builder.Configure().SetClientToken("Your Client Token Here"); + builder.Configure().SetRegionCode(1); // 0: CN, 1: GLOBAL + builder.AddProduct("100_gold_coins", ProductType.Consumable); + + UnityPurchasing.Initialize (this, builder); + } + + /// + /// Called when Unity IAP is ready to make purchases. + /// + public void OnInitialized (IStoreController controller, IExtensionProvider extensions) + { + this.controller = controller; + this.extensions = extensions; + } + + /// + /// Called when Unity IAP encounters an unrecoverable initialization error. + /// + /// Note that this will not be called if Internet is unavailable; Unity IAP + /// will attempt initialization until it becomes available. + /// + public void OnInitializeFailed (InitializationFailureReason error) + { + } + + public void OnInitializeFailed(InitializationFailureReason error, string str) + { + } + + + /// + /// Called when a purchase completes. + /// + /// May be called at any time after OnInitialized(). + /// + public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e) + { + return PurchaseProcessingResult.Complete; + } + + /// + /// Called when a purchase fails. + /// + public void OnPurchaseFailed (Product i, PurchaseFailureReason p) + { + } +} +``` + +### 发起内购流程 + +在 IAP 被成功初始化以后,你可以通过调用 `IStoreController.InitiatePurchase` 来发起内购流程。 + +```cs +// Example method called when the user taps a 'buy' button +// to start the purchase process. +public void OnPurchaseClicked(string productId) { + controller.InitiatePurchase(productId, "developerPayload, e.g. user_id or order_id"); +} +``` + +### 处理购买结果 + +购买完成时会调用商店监听器的 ProcessPurchase 函数。无论用户购买任何物品,您的应用程序都应该履单;例如,解锁本地内容或将购买收据发送给服务器以更新服务器端游戏模型。 + +此过程会返回结果以指出应用程序是否已完成对购买的处理: + +| 结果 | 描述 | +| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| PurchaseProcessingResult.Complete | 应用程序已完成对购买的处理,不应再次向应用程序通知此事。 | +| PurchaseProcessingResult.Pending | 应用程序仍在处理购买,除非调用 `IStoreController` 的 `ConfirmPendingPurchase` 函数,否则将在下一次应用程序启动时再次调用 `ProcessPurchase`。 | + +请注意,在初始化成功后,随时可能调用 `ProcessPurchase`。如果应用程序在 `ProcessPurchase` 处理程序执行过程中崩溃,那么在 Unity IAP 下次初始化时会再次调用它,因此您可能希望实现自己的额外重复数据删除功能。 + +#### 可靠性 + +Unity IAP 要求明确确认购买以确保在网络中断或应用程序崩溃的情况下可靠地完成购买。在应用程序离线时完成的任何购买都将在下次初始化时发送给应用程序。 + +#### 立即完成购买 + +返回 `PurchaseProcessingResult.Complete` 时,Unity IAP 立即完成交易(如下图所示)。 + +如果您正在销售可消耗商品并从服务器履行订单(例如,在网络游戏中提供游戏币),那么您不得返回 `PurchaseProcessingResult.Complete`。 + +否则,如果在保存到云端之前卸载应用程序,则购买的消耗品将面临丢失的风险。 + +![](https://capacity-files.lcfile.com/PYPIumvCKtaAwhJOPAf1qaApNMOdQ9nF/VrHVbfdKUoCHgXxmOYkccZLMn8e.png) + +#### 将购买保存到云端 + +如果要将消耗品购买交易保存到云端,您必须返回 `PurchaseProcessingResult.Pending`,并且仅在成功保存购买时才调用 `ConfirmPendingPurchase`。 + +返回 `Pending` 时,Unity IAP 会在底层商店中保持交易为未结 (open) 状态,直至确认为已处理为止,因此确保了即使在消耗品处于此待处理状态时用户重新安装您的应用程序,消耗品购买交易也不会丢失。 + +![](https://capacity-files.lcfile.com/zDX7oMjWBju6ME0K9i4zym9OOuIgCvrx/TFgMbu4JhohID1xHBnhcet7nnth.png) + +## 调试 +**我们当前只提供了 Android 的库实现, 请在 Android 环境下进行各功能调试**(其他平台会逐步补充完善)。 \ No newline at end of file diff --git a/cn/docs/sdk/tap-iap/faq.mdx b/cn/docs/sdk/tap-iap/faq.mdx new file mode 100644 index 000000000..0b34bd68a --- /dev/null +++ b/cn/docs/sdk/tap-iap/faq.mdx @@ -0,0 +1,37 @@ +--- +title: 常见问题 +sidebar_position: 5 +--- + +### 1、Unity 2020.3.15 之前的版本升级 Gradle 版本的操作步骤 + +在 `Player Settings` -> `Player` -> `Android Tab` -> `Publish Settings` -> `Build`,然后勾选 **Custom Base Gradle Template**、**Custom Launcher Gradle Template**、**Custom Main Gradle Template**。 + +![](https://capacity-files.lcfile.com/5nSUEX6IWVRkwu4Nep1pXo6vUnl9VppH/%E5%8D%87%E7%BA%A7gradle_1.png) + +以下更改应用于 `Assets/Plugins/Android/` 该文件夹下生成的三个文件 `mainTemplate.gradle`、`launcherTemplate.gradle`、`baseProjectTemplate.gradle`。 + +打开 `baseProjectTemplate.gradle` 修改文件内容: + +```groovy +dependencies { + ... + // 将 3.x.0 版本修改为 4.0.0 + //classpath 'com.android.tools.build:gradle:3.x.0' + classpath 'com.android.tools.build:gradle:4.0.0' +} +``` + +分别打开 `mainTemplate.gradle` 和 `launcherTemplate.gradle` 文件,找到 `lintOptions` 标签, 分别添加 **checkReleaseBuilds false** 配置: + +```groovy +lintOptions { + abortOnError false + checkReleaseBuilds false +} +``` + + +同时,为了将 [Gradle 版本和 Android Gradle Plugin 版本对应](https://developer.android.com/studio/releases/gradle-plugin#expandable-1),需要更新 Gradle 版本,下载 [6.5.0 版本的 Gradle](https://services.gradle.org/distributions/gradle-6.5-all.zip),解压后放到自定义的文件夹中,同时**不**勾选 Unity 中的 `Preferences` -> `External Tools`-> `Android` -> `Gradle Installed with Unity(recommend)`,改为选择解压后 Gradle 文件夹的位置,如 `/gradle-6.5.0`。 + +![](https://capacity-files.lcfile.com/hrkFCRy9VuLapvsanFm6nhpkHEEz0qVE/%E5%8D%87%E7%BA%A7gradle_2.png) \ No newline at end of file diff --git a/cn/docs/sdk/tap-iap/operating-manual.mdx b/cn/docs/sdk/tap-iap/operating-manual.mdx new file mode 100644 index 000000000..4d50af52d --- /dev/null +++ b/cn/docs/sdk/tap-iap/operating-manual.mdx @@ -0,0 +1,108 @@ +--- +title: 系统操作手册 +sidebar_position: 4 +--- + +## 一、商品管理 + +若要在游戏中提供内购商品,必须在 TapTap 开发者服务中心录入内购商品信息,并添加到你的游戏内。每个商品须关联一个游戏应用(Client),并且只能用于该游戏。在使用过程中,你可以创建或删除商品,也可以修改完善现有商品的信息。 + +### 创建游戏内商品 + +在创建商品之前,请务必仔细规划您的产品 ID。商品 ID 对于您的游戏来说必须是唯一的,并且在创建后不能更改或重复使用。 + +- 商品 ID 必须以数字或字母开头,长度不超过 30 个字符,可以包含数字 (0-9)、英文字母 (a-z/A-Z)、下划线 (_) 和句点 (.) +- 创建商品后,您无法更改或重复使用商品 ID + +#### 创建单个商品 + +1. 前往 TapTap 开发者服务中心,选择「游戏服务」-「内购服务」; + +2. 点击「添加商品」,在弹出的表单中填写您的商品详细信息; + + - 商品类型:消耗型和非消耗型商品,详情请参见[商品类型](/sdk/tap-iap/overview) + - 商品 ID:商品的唯一标识,创建后不可更改和重复使用; + - 商品名称:商品的短名称,用户可见。建议保持在 80 个字符以内; + - 商品描述:对商品内容的细节描述,建议保持在 100 个字符以内; + - 商品定价:期望售卖的价格,默认以美元定价,开发者可下载所有定价等级查看不同国家的价格; + +3. 完成信息填写后,点击「提交审核」并等待审核结果; + +4. 审核通过后,如需商品生效,可点击商品对应操作列的「上架」按钮即可; + + +> 注意同一个游戏内商品 ID 不能重复,提交后将不可更改,删除后也无法重复使用,请仔细规划。商品在未上架状态时均可修改名称、描述、价格等信息,上架后的商品不可修改信息。具体见「编辑/修改商品」。 + + +![mahua](https://capacity-files.lcfile.com/GSpWNUzJ025ypkp42jD180Ko7u9wDiUA/add_sku.png) + +### 批量创建多个商品 + +1. 如需同时创建多个游戏内购商品,您可以选择使用「批量导入」功能,上传包含每个商品详细信息的 CSV 文件; + +2. 前往 TapTap 开发者服务中心,选择「游戏服务」-「内购服务」; + +3. 点击「批量导入」: + + - 下载模版; + - 上传商品详细信息 CSV 文件; + - 请确保每个商品都为单独一行,单次上传不得超过 100 个商品,下载模板; + - 当商品详细信息 CSV 中的商品 ID 与后台列表中现有商品 ID 匹配时,此次商品详细信息 CSV 中的商品不被上传; + +> 商品详细信息 CSV 文件上传成功后,系统将直接提交审核,若上传了系统不支持的语言,则会忽略该语言内容并由默认语言替代。 + +![tappay](https://capacity-files.lcfile.com/6GwDJdk5zVDaVp1znwrPRUEx1TcmjQC5/add_sku_batch.png) + +### 编辑/修改商品 + +| 商品状态 | 编辑与操作 | +| ---- | ---- | +| 审核通过,待上架 | 可修改商品名称、商品说明 / 可上架 / 可删除 | +| 审核失败 | 可修改商品名称、商品说明 / 可删除 | +| 已上架 | 可修改商品名称、商品说明 / 可下架 | +| 已下架 | 可删除 | + +### 商品状态说明 + +* __审核中__:商品在发布之前需要提交审核,审核通过则进入待上架状态,审核失败会给出失败原因,需要根据失败原因进行修改后重新提交; + +* __审核通过__:审核通过的商品,处于待上架状态,可随时发布上架,上架的商品才会在生产环境生效; + +* __已上架__:上架后的商品,会在生产环境生效; + +* __已下架__:下架的商品,无法在生产环境使用。如用户点击该商品后提示“该商品已下架”; + +* __已删除__:开发者可以删除商品,删除的商品不可再复用; + + +## 二、管理订单 + +你可以通过 TapTap 开发者服务中心的「游戏服务」-「内购服务」-「交易列表」来查看游戏内购买商品(IAP)的销售订单 + +![tappay](https://capacity-files.lcfile.com/8WMyxhKLhNA1SniirMSVSOt56kS2z2wb/tappay-orders.png) + +### 查找订单 + +TapTap IAP 支持多种查询方式: + +* __按用户标识查询__:输入用户的 TapTap ID,即可查询到当前用户购买的所有订单; + +* __按订单号查询__:用户在游戏内购买时创建的 TapTap 订单号; + +* __按交易流水号查询__:开发者自定义的订单 ID; + +* __按订单状态查询__:根据订单交易状态查询,包括“支付中”、“已支付”、“已验证”、“已过期”、“已退款”等; + +* __按订单时间查询__:筛选用户订单发生的日期查询; + +### 订单状态 + +订单页面会显示每一笔用户购买的状态,如下: + +| 订单支付状态 | 说明 | +| ---- | ---- | +| 支付中 | 用户还未完成付款,或系统正在处理订单 | +| 订单已支付 | 用户完成付款,系统已成功向用户收款 | +| 订单已过期 | 订单长时间未进行付款,或付款已取消 | + + diff --git a/cn/docs/sdk/tap-iap/overview.mdx b/cn/docs/sdk/tap-iap/overview.mdx new file mode 100644 index 000000000..4575ed715 --- /dev/null +++ b/cn/docs/sdk/tap-iap/overview.mdx @@ -0,0 +1,42 @@ +--- +title: 游戏内购服务功能介绍 +sidebar_label: 功能介绍 +sidebar_position: 1 +--- + +TapTap 游戏内购服务为开发者提供了便捷高效的解决方案。接入该服务后,开发者可轻松开放游戏内购,无论是消耗型道具还是非消耗型道具,都能方便地上架售卖。同时,订单查看功能,让开发者对销售情况一目了然。 + +## 业务介绍 + +TapTap 游戏内购服务为开发者提供便捷高效的支付解决方案,其特点如下: + +* **多种支付渠道** :接入 SDK 即可支持微信支付和支付宝支付。 +* **灵活配置** :可在开发者后台配置商品及其价格,并且支持通过后台调整。 +* **商品类型多样** :支持消耗型(如金币、钻石)商品和非消耗型(如皮肤)商品。 +* **零分成** :TapTap 对游戏内购服务实行零分成。*但请注意,交易依旧需向微信和支付宝支付 1% 的手续费。* + +## 如何接入和使用 + +### 创建商户 + +开通游戏内购服务需提交材料,入驻成为 TapTap 服务商下的二级商户。请按照《[创建商户](/sdk/tap-iap/create-merchant)》文档的要求准备材料,并将材料发送至邮箱:[PaymentSupport@taptap.com](mailto:PaymentSupport@taptap.com) 完成商户创建。 + +创建商户大约需要 1~2 周,建议您提前完成准备工作。 + +### 接入 SDK + +将 TapTap IAP SDK 集成到您的游戏中,我们提供了 Unity 和 Android 的集成选项: + +- [Unity 集成指南](/sdk/tap-iap/develop/unity) +- [Android 集成指南](/sdk/tap-iap/develop/android) + + +### 上架商品 + +在 [TapTap 开发者中心](https://developer.taptap.io/)添加商品并注册相关信息:项目 ID、项目标题、项目类型、价格等。进一步了解请查阅《[系统操作手册](/sdk/tap-iap/operating-manual)》。 + +### 订单结算 + +系统将自动在订单交易成功后 7 个自然日完成结算,结算后资金将自动转入您的微信和支付宝商户账户,您可以登录微信和支付宝的商户后台查看账单、操作提现等。 + +您也可以设置自动提现,详细可查阅微信和支付宝的商户操作手册。 \ No newline at end of file diff --git a/cn/src/docComponents/v4SDKVersions.ts b/cn/src/docComponents/v4SDKVersions.ts index 54cf38b88..41ea6d6e3 100644 --- a/cn/src/docComponents/v4SDKVersions.ts +++ b/cn/src/docComponents/v4SDKVersions.ts @@ -1,6 +1,6 @@ -const taptapUnity = "4.4.3" -const taptapIos = "4.4.3" -const taptapAndroid = "4.4.3" +const taptapUnity = "4.5.0" +const taptapIos = "4.5.0" +const taptapAndroid = "4.5.0" const taptapUnreal = "3.29.2" const v4SDKVersions = { diff --git a/hk/docs/sdk/TapPayments/develop/unity.mdx b/hk/docs/sdk/TapPayments/develop/unity.mdx index 34edbfe7d..05120d66f 100644 --- a/hk/docs/sdk/TapPayments/develop/unity.mdx +++ b/hk/docs/sdk/TapPayments/develop/unity.mdx @@ -150,11 +150,7 @@ public class MyIAPManager : IStoreListener { builder.Configure().SetClientId("Your Client ID Here"); builder.Configure().SetClientToken("Your Client Token Here"); builder.Configure().SetRegionCode(1); // 0: CN, 1: GLOBAL - builder.AddProduct("100_gold_coins", ProductType.Consumable, new IDs - { - {"100_gold_coins_google", GooglePlay.Name}, - {"100_gold_coins_mac", MacAppStore.Name} - }); + builder.AddProduct("100_gold_coins", ProductType.Consumable); UnityPurchasing.Initialize (this, builder); } diff --git a/hk/i18n/en/docusaurus-plugin-content-docs/current/sdk/TapPayments/develop/unity.mdx b/hk/i18n/en/docusaurus-plugin-content-docs/current/sdk/TapPayments/develop/unity.mdx index 4f4045e81..54ede3caa 100644 --- a/hk/i18n/en/docusaurus-plugin-content-docs/current/sdk/TapPayments/develop/unity.mdx +++ b/hk/i18n/en/docusaurus-plugin-content-docs/current/sdk/TapPayments/develop/unity.mdx @@ -145,11 +145,7 @@ public class MyIAPManager : IStoreListener { builder.Configure().SetClientId("Your Client ID Here"); builder.Configure().SetClientToken("Your Client Token Here"); builder.Configure().SetRegionCode(1); // 0: CN, 1: GLOBAL - builder.AddProduct("100_gold_coins", ProductType.Consumable, new IDs - { - {"100_gold_coins_google", GooglePlay.Name}, - {"100_gold_coins_mac", MacAppStore.Name} - }); + builder.AddProduct("100_gold_coins", ProductType.Consumable); UnityPurchasing.Initialize (this, builder); }