需要详细步骤请查看:
Unity 之 接入IOS内购过程解析
Unity内购官方文档
Mac支付和IOS逻辑基本一致,这是我之前做IOS内购时的思维导图,可以看下,先有个概念:
创建四个按钮,分别为购买道具
,清空日志
,购买非消耗道具
,恢复购买
;为了方便查看日志,我还创建了一个ScrollView
组件下面放了一个Text接受日志输出。
完整代码如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.UI;
///
/// IAP管理类
///
public class IAPManagerTest : MonoBehaviour, IStoreListener
{
public Text riZhiText;
///
/// 需要换成对应游戏后台的key
///
private string[] goodsList = new string[]
{
"com.Czhenya.zuan10",
};
///
/// 非消耗型道具 -- 去除广告的id
///
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();
}
///
/// 初始化
///
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);
}
///
/// 初始化成功 -- 接口函数
///
///
///
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";
}
///
/// 初始化失败回调 -- 接口函数
///
///
public void OnInitializeFailed(InitializationFailureReason error)
{
Debug.LogError("【Unity IAP】初始化失败 OnInitializeFailed, reason:" + error.ToString());
riZhiText.text += "【Unity IAP】初始化失败 OnInitializeFailed, reason:" + error.ToString() + "\n";
}
///
/// 购买失败回调 -- 接口函数
///
///
///
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;
}
}
///
/// 购买成功回调 -- 接口函数
///
///
///
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;
}
///
/// 支付失败回调
///
private Action onPurchaseFailed;
///
/// 支付成功回调
///
private Action<string> onPurchaseSuccess;
///
/// 购买产品
///
/// 产品ID
/// 失败回调
/// 成功回调
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();
}
}
}
///
/// 发起购买函数 -- 商城按钮监听
///
///
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 购买回复非消耗道具
///
/// 购买非消耗道具 -- 商城按钮监听
///
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);
}
///
/// 恢复购买非消耗道具 -- 商城按钮监听
///
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
///
/// 购买失败回调
///
void OnBuyFailed()
{
Debug.Log("【Unity IAP】购买失败回调 OnBuyFailed...");
riZhiText.text += "【Unity IAP】购买失败回调 OnBuyFailed... \n";
}
///
/// 购买成功回调
///
///
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
面板下拖拽赋值。正式使用时可自行删除注释或者点击获取源码。
将包名修改为与后台一致,其他属性默认即可。若需要更多设置,可参考:Unity 之 打包参数 – Player面板属性详解
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"
productbuild --component /Users/Czhenya/Desktop/Mac/你的.app /Applications --sign "3rd Party Mac Developer Installer: 证书." /Users/Czhenya/Desktop/Mac/你的.pkg
若是单机游戏无需服务器进行支付验证,则按照成功回调发放奖励跳过此步骤即可。若需要服务器验证,则将支付成功的Payload传到服务器,获取验证结果后发放奖励或提示支付失败。
服务端验证返回数据
iOS发起票据验证请求后,通过处理AppStore返回数据来验单。服务验证需要注意的地方:不同iOS版本的返回数据不同,服务端验证方式也不同。
{
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
}
{
receipt = {
"bid" = "com.Czhenya",
"bvrs" = 1,
"item_id" = 573837050,
"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
},
status = 0
}
验证订单是否成功,关键看这几个数据:
AppStore 服务器有两个,对应测试环境(沙盒测试)和正式环境:
状态码 | 说明 |
---|---|
21000 | 未使用HTTP POST请求方法向App Store发送请求。 |
21001 | 此状态代码不再由App Store发送。 |
21002 | receipt-data属性中的数据格式错误,或者服务遇到了临时问题。再试一次。 |
21003 | 收据无法认证。 |
21004 | 您提供的共享密钥与您帐户的文件共享密钥不匹配。 |
21005 | 收据服务器暂时无法提供收据。再试一次。 |
21006 | 该收据有效,但订阅已过期。当此状态码返回到您的服务器时,收据数据也会被解码并作为响应的一部分返回。仅针对自动续订的iOS 6样式的交易收据返回。 |
21007 | 该收据来自测试环境,但是已发送到生产环境以进行验证。 |
21008 | 该收据来自生产环境,但是已发送到测试环境以进行验证。 |
21009 | 内部数据访问错误。稍后再试。 |
21010: | 找不到或删除了该用户帐户。 |
源码和步骤都在上面分享过了,若还有什么不明白的,可以点击链接下载,积分不够的童鞋关注下方卡片公号,回复:Mac内购 即可获得Demo源码~