Unity 之 Mac App Store 内购过程解析(购买非消耗道具 | 恢复购买 | 支付验证)
Posted 陈言必行
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity 之 Mac App Store 内购过程解析(购买非消耗道具 | 恢复购买 | 支付验证)相关的知识,希望对你有一定的参考价值。
Unity 之 Mac App Store 内购过程解析(恢复购买)
准备工作
- 苹果后台设置
- 创建工程导入内购插件
需要详细步骤请查看:
Unity 之 接入IOS内购过程解析
Mac支付和ios逻辑基本一致,这是我之前做IOS内购时的思维导图,可以看下,先有个概念:
一,具体实现
1.1 场景搭建
创建四个按钮,分别为购买道具
,清空日志
,购买非消耗道具
,恢复购买
;为了方便查看日志,我还创建了一个ScrollView
组件下面放了一个Text接受日志输出。
创建完成效果如下:
1.2 代码实现
完整代码如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.UI;
/// <summary>
/// IAP管理类
/// </summary>
public class IAPManagerTest : MonoBehaviour, IStoreListener
public Text riZhiText;
/// <summary>
/// 需要换成对应游戏后台的key
/// </summary>
private string[] goodsList = new string[]
"com.Czhenya.zuan10",
;
/// <summary>
/// 非消耗型道具 -- 去除广告的id
/// </summary>
private string removedsId = "com.Czhenya.delad";
private bool isRestore = false;
// 控制器
private IStoreController controller;
// 苹果扩展
private IAppleExtensions appleExtensions;
// 谷歌商店扩展
private IGooglePlayStoreExtensions googlePlayStoreExtensions;
private static IExtensionProvider extensionProvider;
// 是否可以发起购买
private bool isCanOnClickBubBtn = false;
void Start()
Application.targetFrameRate = 60;
Init();
/// <summary>
/// 初始化
/// </summary>
private void Init()
// 没有网络,IAP会一直初始化
if (Application.internetReachability == NetworkReachability.NotReachable)
Debug.Log("----- 用户没有连接网络 IAP不可用 ------");
riZhiText.text += "----- 用户没有连接网络 IAP不可用 ------\\n";
var module = StandardPurchasingModule.Instance();
ConfigurationBuilder builder = ConfigurationBuilder.Instance(module);
// builder.AddProduct("商品id1", ProductType.Consumable);
// ProductType :和后台说明对应
// consumable:可消费的,如游戏中的金币,用完还可以再购买。
// non-consumable:不可销毁的,一次购买,永久生效。比如去广告,解锁游戏关卡,这种商品只能购买一次。
// subscription:订阅的,这种一般用于新闻、杂志、或者app里面的月卡。可以按月或者按年收费。
for (int i = 0; i < goodsList.Length; i++)
builder.AddProduct(goodsList[i], ProductType.Consumable);
// 不可销毁的,一次购买,永久生效。比如去广告,解锁游戏关卡,这种商品只能购买一次。
builder.AddProduct(removedsId, ProductType.NonConsumable);
riZhiText.text += "----- 开始初始化... ------\\n";
// 开始初始化
UnityPurchasing.Initialize(this, builder);
/// <summary>
/// 初始化成功 -- 接口函数
/// </summary>
/// <param name="controller"></param>
/// <param name="extensions"></param>
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
Debug.Log("【Unity IAP】初始化成功 IAP initialize success");
riZhiText.text += "【Unity IAP】初始化成功 IAP initialize success\\n";
isCanOnClickBubBtn = true;
this.controller = controller;
// 回调赋值
extensionProvider = extensions;
appleExtensions = extensions.GetExtension<IAppleExtensions>();
googlePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();
//登记 购买延迟 监听器
appleExtensions.RegisterPurchaseDeferredListener(OnDeferred);
//购买延迟提示
private void OnDeferred(Product item)
Debug.Log("【Unity IAP】 网速慢.................");
riZhiText.text += "【Unity IAP】 网速慢.................\\n";
/// <summary>
/// 初始化失败回调 -- 接口函数
/// </summary>
/// <param name="error"></param>
public void OnInitializeFailed(InitializationFailureReason error)
Debug.LogError("【Unity IAP】初始化失败 OnInitializeFailed, reason:" + error.ToString());
riZhiText.text += "【Unity IAP】初始化失败 OnInitializeFailed, reason:" + error.ToString() + "\\n";
/// <summary>
/// 购买失败回调 -- 接口函数
/// </summary>
/// <param name="i"></param>
/// <param name="p"></param>
public void OnPurchaseFailed(Product i, PurchaseFailureReason p)
Debug.LogError("【Unity IAP】购买失败 OnPurchaseFailed,reason:" + p.ToString());
riZhiText.text += "【Unity IAP】购买失败 OnPurchaseFailed,reason:" + p.ToString() + "\\n";
if (this.onPurchaseFailed != null)
this.onPurchaseFailed();
this.onPurchaseFailed = null;
/// <summary>
/// 购买成功回调 -- 接口函数
/// </summary>
/// <param name="e"></param>
/// <returns></returns>
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
Debug.Log("【Unity IAP】购买成功 purchase finished, apple return receipt:" + e.purchasedProduct.receipt);
//riZhiText.text += "【Unity IAP】购买成功 purchase finished, apple return receipt:" + e.purchasedProduct.receipt + "\\n";
riZhiText.text += "【Unity IAP】购买成功 e.purchasedProduct.definition.id:" + e.purchasedProduct.definition.id + "\\n";
riZhiText.text += "【Unity IAP】恢复购买成功 isRestore: " + isRestore + "\\n";
if (isRestore) // 恢复购买
Debug.Log("恢复购买成功 isRestore " + isRestore);
// 判断是否是去除广告id
if (removedsId.Equals(e.purchasedProduct.definition.id))
Debug.Log("恢复购买成功");
// todo... 恢复成功回调
isRestore = false;
else
onPurchaseFailed?.Invoke();
return PurchaseProcessingResult.Complete;
if (this.onPurchaseSuccess != null)
this.onPurchaseSuccess(e.purchasedProduct.receipt);
this.onPurchaseSuccess = null;
return PurchaseProcessingResult.Complete;
/// <summary>
/// 支付失败回调
/// </summary>
private Action onPurchaseFailed;
/// <summary>
/// 支付成功回调
/// </summary>
private Action<string> onPurchaseSuccess;
/// <summary>
/// 购买产品
/// </summary>
/// <param name="productId">产品ID</param>
/// <param name="onFailed">失败回调</param>
/// <param name="onSuccess">成功回调</param>
public void PurchaseProduct(string productId, Action onFailed, Action<string> onSuccess)
this.onPurchaseFailed = onFailed;
this.onPurchaseSuccess = onSuccess;
if (controller != null)
var product = controller.products.WithID(productId);
if (product != null && product.availableToPurchase)
Debug.Log("【Unity IAP】开始购买");
riZhiText.text += "【Unity IAP】开始购买... \\n";
controller.InitiatePurchase(productId);
else
Debug.LogError("【Unity IAP】失败回调 no product with productId:" + productId);
riZhiText.text += "【Unity IAP】失败回调 no product with productId:" + productId + " \\n";
if (this.onPurchaseFailed != null)
this.onPurchaseFailed();
else
Debug.LogError("【Unity IAP】失败回调 controller is null,can not do purchase");
riZhiText.text += "Unity IAP】失败回调 controller is null,can not do purchase \\n";
if (this.onPurchaseFailed != null)
this.onPurchaseFailed();
/// <summary>
/// 发起购买函数 -- 商城按钮监听
/// </summary>
/// <param name="i"></param>
public void OnClickPurchase(int i)
// 正式项目时需限制 -- 不允许多次点击
Debug.Log("【Unity IAP】发起购买函数 " + Application.internetReachability);
riZhiText.text += "【Unity IAP】发起购买函数 "+Application.internetReachability+" \\n";
if (Application.internetReachability == NetworkReachability.NotReachable)
Debug.Log("【Unity IAP】用户没网... ");
return;
PurchaseProduct(goodsList[0], OnBuyFailed, OnBuySuccess);
#region 购买回复非消耗道具
/// <summary>
/// 购买非消耗道具 -- 商城按钮监听
/// </summary>
public void OnClickRemoved()
// 正式项目时需限制 -- 不允许多次点击
Debug.Log("【Unity IAP】购买一次性道具 " + Application.internetReachability);
riZhiText.text += "【Unity IAP】购买一次性道具 "+Application.internetReachability+" \\n";
if (Application.internetReachability == NetworkReachability.NotReachable)
Debug.Log("【Unity IAP】用户没网... ");
return;
PurchaseProduct(removedsId, OnBuyFailed, OnBuySuccess);
/// <summary>
/// 恢复购买非消耗道具 -- 商城按钮监听
/// </summary>
public void OnClickRecover()
// 正式项目时需限制 -- 不允许多次点击
Debug.Log("【Unity IAP】恢复购买 " + Application.internetReachability);
riZhiText.text += "【Unity IAP】恢复购买 "+Application.internetReachability+" \\n";
if (Application.internetReachability == NetworkReachability.NotReachable)
Debug.Log("【Unity IAP】用户没网... ");
return;
if (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.OSXPlayer)
Debug.Log("发起恢复请求");
isRestore = true;
IAppleExtensions apple = extensionProvider.GetExtension<IAppleExtensions>();
apple.RestoreTransactions(HandleRestored);
else
Debug.Log("恢复购买失败. 不支持这个平台. 当前平台 = " + Application.platform);
// 恢复购买之后,会返回一个状态,如果状态为true,
// 之前购买的非消耗物品都会回调一次购买成功(ProcessPurchase)
// 然后在这里个回调里面进行处理
void HandleRestored(bool result)
// 返回一个bool值,如果成功,则会多次调用支付回调,然后根据支付回调中的参数得到商品id,最后做处理(ProcessPurchase)
Debug.Log("恢复购买继续: " + result + ". 如果没有进一步的消息,则没有可恢复的购买。");
isRestore = result;
riZhiText.text += "【Unity IAP】恢复购买继续 " + result + ". 如果没有进一步的消息,则没有可恢复的购买。 \\n";
if (result)
riZhiText.text += "【Unity IAP】恢复购买成功! \\n";
Debug.Log("恢复购买成功!");
else
riZhiText.text += "【Unity IAP】恢复购买失败! \\n";
Debug.Log("恢复购买失败!");
// todo...回调处理
#endregion
/// <summary>
/// 购买失败回调
/// </summary>
void OnBuyFailed()
Debug.Log("【Unity IAP】购买失败回调 OnBuyFailed...");
riZhiText.text += "【Unity IAP】购买失败回调 OnBuyFailed... \\n";
/// <summary>
/// 购买成功回调
/// </summary>
/// <param name="str"></param>
void OnBuySuccess(string str)
Debug.Log("【Unity IAP】购买成功回调 OnBuySuccess..." + str);
riZhiText.text += "【Unity IAP】购买成功回调 OnBuySuccess... \\n";
riZhiText.text += "【Unity IAP】购买成功...收据: " + str + " \\n";
//会得到下面这样一个字符串
//"Store":"AppleAppStore",
//"TransactionID":"1000000845663422",
//"Payload":"MIIT8QYJKoZIhvcNAQcCoIIT4jCCE94CAQExBBMMIIBa ... 还有N多 ..."
public void ClearRiZhi()
riZhiText.text = "清空数据\\n";
PS:此代码为上图使用的测试代码,按钮点击监听赋值,在Inspector
面板下拖拽赋值。正式使用时可自行删除注释或者点击获取源码。
1.3 打包设置
将包名修改为与后台一致,其他属性默认即可。若需要更多设置,可参考:Unity 之 打包参数 – Player面板属性详解
二,打包测试
2.1 实现步骤说明
Mac内购流程打包步骤
- 使用正式包名
和苹果后台创建的对应上,直接在Unity里面设置好。 - 签名app并打包为pkg
若在Unity中没有设置正确包名,也可以直接在打包处理的app,右键显示包内容,找到信息.plist文件并将CFBundleIdentifier字符串更新为应用程序的包名。
在2.2中还有详细的签名和导出pkg的步骤 - 安装pkg并调用初始化内购项
要正确安装软件包,请删除未打包的。运行新创建的软件包并安装它之前的应用程序文件。
在3.1中有测试步骤实现过程 - 调用购买并尝试购买查看返回数据
测试结果和支付验证
2.2 Mac签名命令
签名需要两个证书和一个签名文件,若之前都没搞过,则可以参考:Unity 之 上传Mac App Store过程详解
文章中有详细获取证书步骤和签名配置所需文件。
- 设置权限
chmod -R a+xr "/Users/Czhenya/Desktop/Mac/你的.app"
- 文件签名
codesign -o runtime -f --deep -s '3rd Party Mac Developer Application: 证书.' --entitlements "/Users/Czhenya/Desktop/App.entitlements" "/Users/Czhenya/Desktop/Mac/你的.app"
- 打包pkg
productbuild --component /Users/Czhenya/Desktop/Mac/你的.app /Applications --sign "3rd Party Mac Developer Installer: 证书." /Users/Czhenya/Desktop/Mac/你的.pkg
三,示例演示
3.1 购买商品
-
商品初始化成功:
-
输入沙盒账号:(首次使用会有双重认证之类的确保身份安全,可选择跳过或按照提示操作即可)
-
若是第一次登录,需要确认Apple ID 安全,点击继续:
-
若出现“保护您的账号”提示,选择不升级即可:
-
最后终于到了支付购买界面了,点击购买就可了:
-
购买完成后,会弹出操作完成提示,点击“好“即可触发支付成功回调(沙盒会稍微慢一点)
-
支付成功回调:
-
取消支付回调:
3.2 购买非消耗道具
- 初始化成功后,点击购买非消耗道具
3.3 恢复购买
- 购买过一次之后,再次购买会购买失败,这时需要点击恢复购买,执行恢复购买逻辑
四,支付验证
若是单机游戏无需服务器进行支付验证,则按照成功回调发放奖励跳过此步骤即可。若需要服务器验证,则将支付成功的Payload传到服务器,获取验证结果后发放奖励或提示支付失败。
4.1 验证返回数据
服务端验证返回数据
iOS发起票据验证请求后,通过处理AppStore返回数据来验单。服务验证需要注意的地方:不同iOS版本的返回数据不同,服务端验证方式也不同。
- iOS7及以上获取的票据返回数据:
receipt =
"adam_id" = 0,
"app_item_id" = 0,
"application_version" = 1,
"bundle_id" = "com.Czhenya",
"download_id" = 0,
"in_app" =
"is_trial_period" = false,
"original_purchase_date" = "2022-10-24 01:00:00 Etc/GMT",
"original_purchase_date_ms" = 1483203661000,
"original_purchase_date_pst" = "2022-10-24 01:00:00 America/Los_Angeles",
"original_transaction_id" = 1000000000000001,
"product_id" = "com.Czhenya.zuan10",
"purchase_date" = "2022-10-24 01:00:00 Etc/GMT",
"purchase_date_ms" = 1483203661000,
"purchase_date_pst" = "2022-10-24 01:00:00 America/Los_Angeles",
"transaction_id" = 1000000000000001
,
"receipt_type" = "ProductionSandbox",
"request_date" = "2022-10-24 01:00:00 Etc/GMT",
"request_date_ms" = 1483203661000,
"request_date_pst" = "2022-10-24 01:00:00 America/Los_Angeles",
"version_external_identifier" = 0,
,
status = 0
- iOS7以下获取的票据返回数据(不包括iOS7):
receipt =
"bid" = "com.Czhenya",
"bvrs" = 1,
"item_id" = 573837050,
"original_purchase_date" = 以上是关于Unity 之 Mac App Store 内购过程解析(购买非消耗道具 | 恢复购买 | 支付验证)的主要内容,如果未能解决你的问题,请参考以下文章