1.开发环境
1、MacBook Pro,Apple M1 Pro,macOS Sonoma 14.3.1
2、Unity Hub 版本3.7.0(3.7.0)
3、unity Version 2020.3.28f1 Personal
4、In App Purchasing Package v4.1.5
2.开发 IAP
2.1 获取开发资料
1、根据使用的unity IDE版本选择对应的开发文档,该链接为unity 2020.3.28f1的IAP开发文档
1)在该文档的左上角可以选择不同Unity 版本对应的开发文档,选择你所需要的即可:
2、根据你安装的In App Purchasing版本选择对应的开发文档,该链接为 4.1.5版本的开发文档
- Unity提供的 In App Purchasing最新版本为4.10.0,无论哪个版本都封装的是Apple Store Kit v1,无法使用Apple storeKit2的新特性。
3、在 Apple Store Connect 中创建App和商品ID,并保存 Bundle Identifier 和 商品ID,在初始化App服务时会用到。
2.2 项目配置
1、添加IAP Package
2、打开IAP Service服务
2.3 IAP支付代码
sing System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NtUtils;
using UnityEngine;
using UnityEngine.Purchasing;
namespace NtSDK
{
public class NtIAPManager : IStoreListener
{
private const string Tag = "[NtIAPManager]";
private IStoreController m_Controller;
private IAppleExtensions m_AppleExtensions;
private SkuDetailCallback m_SkuDetailCallback;
private NtIAPCallback m_IapCallback;
private string[] m_IapIdList;
private string m_ErrorMsg = NtCommonInstance<NtLocalizationManager>.Instance.GetValueByKey(
"IDS_ERROR_CODE_-1");
// 获取商品详情
// 获取商品详情的本质就是利用商品ID初始化Unity IAP服务的,商品ID必须是真实有效的
internal void GetSkuDetails(String iapIDs, SkuDetailCallback skuDetailCallback)
{
NtLog.Log(NtLog.NtLogLevel.Log, Tag, "start get sku details");
m_SkuDetailCallback = skuDetailCallback;
if (string.IsNullOrEmpty(iapIDs))
{
NtLog.Log(NtLog.NtLogLevel.Error, Tag, "start get sku details fail, iapIDs is nil");
skuDetailCallback?.onFailed(NtErrorCode.Failed, m_ErrorMsg);
return;
}
string[] iapIdList = iapIDs.Split(';');
m_IapIdList = iapIdList;
InitUnityIAP(false);
}
internal void IAPPay(NtIAPPay payInfo, NtIAPCallback iapCallback)
{
m_IapCallback = iapCallback;
var productID = payInfo.productId;
if (m_Controller == null)
{
NtLog.Log(NtLog.NtLogLevel.Error, Tag, "iap pay fail, m_Controller is nil");
OnFailCallback(productID);
return;
}
Product product = m_Controller.products.WithID(productID);
if (product == null || !product.availableToPurchase)
{
NtLog.Log(NtLog.NtLogLevel.Error, Tag, "iap pay fail, product is not available");
OnFailCallback(productID);
return;
}
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
bool canMakePayments = builder.Configure<IAppleConfiguration>().canMakePayments;
if (!canMakePayments)
{
NtLog.Log(NtLog.NtLogLevel.Error, Tag, "iap pay fail, user can not make payments");
OnFailCallback(productID);
return;
}
// 这里根据实际的业务需求:请求服务端接口创建订单
}
// 初始化Unity IAP服务
private void InitUnityIAP(bool isClearFailedOrder)
{
NtLog.Log(NtLog.NtLogLevel.Log, Tag, "start init unity iAP");
if (m_IapIdList == null || m_IapIdList.Length == 0)
{
NtLog.Log(NtLog.NtLogLevel.Error, Tag, "init unity iAP, iapId is nil");
OnFailCallback("", NtErrorCode.Failed, m_ErrorMsg);
return;
}
if (Application.internetReachability == NetworkReachability.NotReachable)
{
NtLog.Log(NtLog.NtLogLevel.Warning, Tag, "没有网络,IAP会一直初始化");
}
m_IsClearFailedOrder = isClearFailedOrder;
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
foreach (string item in m_IapIdList)
{
// 如果只做单平台,比如Mac OS就不需要像官方文档一样传入platform类型
builder.AddProduct(item, ProductType.Consumable);
}
UnityPurchasing.Initialize(this, builder);
}
private void OnFailCallback(string productID, int code = NtErrorCode.Failed, string msg = null)
{
if (!string.IsNullOrEmpty(msg))
{
NtPromptBox.ShowNtPromptBoxContent(msg);
}
m_IapCallback?.onPayFail(code, productID);
}
#region IStoreListener
/// <summary>
/// This will be called when Unity IAP has finished initialising.
/// </summary>
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
var logTip = m_IsClearFailedOrder ? "and may need clear failed order" : "and need get sku details";
NtLog.Log(NtLog.NtLogLevel.Log, Tag, "IAP initialize success " + logTip);
m_Controller = controller;
m_AppleExtensions = extensions.GetExtension<IAppleExtensions>();
m_AppleExtensions.RegisterPurchaseDeferredListener(OnDeferred);
if (m_IsClearFailedOrder)
{
return;
}
List<SkuDetailsInfo> cpSkuDetailsInfos = new List<SkuDetailsInfo>();
foreach (var product in m_Controller.products.all)
{
SkuDetailsInfo skuDetailsInfo = new SkuDetailsInfo();
skuDetailsInfo.skuType = product.definition.type.ToString(); // 商品类型
skuDetailsInfo.productId = product.definition.id; // 商品ID
skuDetailsInfo.productDescription = product.metadata.localizedDescription; // 商品的本地化描述。
skuDetailsInfo.productName = product.metadata.localizedTitle; // 面向消费者的商品名称,用于应用商店 UI。
skuDetailsInfo.price = product.metadata.localizedPriceString; // 面向消费者的价格字符串,包括货币符号,用于应用商店 UI。
skuDetailsInfo.priceAmount = $"{product.metadata.localizedPrice}"; // 内部系统的商品价格值。
skuDetailsInfo.currencyCode = product.metadata.isoCurrencyCode; // 商品本地化货币的 ISO 代码。
cpSkuDetailsInfos.Add(skuDetailsInfo);
NtLog.Log(NtLog.NtLogLevel.Debug, Tag,
$"productId:{skuDetailsInfo.productId},skuType:{product.definition.type.ToString()},productDescription:{skuDetailsInfo.productDescription},productName:{skuDetailsInfo.productName},price:{skuDetailsInfo.price},priceAmount:{skuDetailsInfo.priceAmount},currencyCode:{skuDetailsInfo.currencyCode}");
if (!product.availableToPurchase)
{
NtLog.Log(NtLog.NtLogLevel.Warning, Tag, $"{product.definition.id} is not purchased");
}
}
m_SkuDetailCallback?.onSuccess(cpSkuDetailsInfos);
}
/// <summary>
/// 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.
/// </summary>
public void OnInitializeFailed(InitializationFailureReason error)
{
if (m_IsClearFailedOrder)
{
NtLog.Log(NtLog.NtLogLevel.Log, Tag,
"sdk init iap fail when clear failed order, reason is " + error.ToString());
return;
}
NtLog.Log(NtLog.NtLogLevel.Error, Tag, "IAP initialized fail, reason is " + error.ToString());
m_SkuDetailCallback?.onFailed(NtErrorCode.Failed, error.ToString());
}
/// <summary>
/// This will be called when a purchase completes.
/// </summary>
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
{
NtLog.Log(NtLog.NtLogLevel.Log, Tag, "purchased successfully");
string productID = purchaseEvent.purchasedProduct.definition.id;
string productName = purchaseEvent.purchasedProduct.metadata.localizedTitle;
string transactionID = purchaseEvent.purchasedProduct.transactionID;
string finalReceipt = purchaseEvent.purchasedProduct.receipt;
// 这里需要向服务端校验支付凭据的正确性,只有校验成功才可以结束交易
if (!string.IsNullOrEmpty(finalReceipt))
{
// 这里需要向服务端校验支付凭据的正确性,只有校验成功才可以结束交易
// 此时标记为pending,当交易凭证被服务端校验成功后再确认购买成功
return PurchaseProcessingResult.Pending;
}
return PurchaseProcessingResult.Complete;
}
/// <summary>
/// Called when a purchase fails.
/// </summary>
public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
NtLog.Log(NtLog.NtLogLevel.Error, Tag,
$"IAP purchase fail, productId is {product.definition.id}, transactionID is {product.transactionID}, reason is {failureReason.ToString()}");
}
/// <summary>
/// iOS Specific.
/// This is called as part of Apple's 'Ask to buy' functionality,
/// when a purchase is requested by a minor and referred to a parent
/// for approval.
///
/// When the purchase is approved or rejected, the normal purchase events
/// will fire.
/// </summary>
/// <param name="item">Item.</param>
private void OnDeferred(Product item)
{
NtLog.Log(NtLog.NtLogLevel.Warning, Tag, "Purchase deferred: " + item.definition.id);
}
#endregion
}
}
3.问题
1、在实际测试中发现,当Unity应用失去焦点后,就接收不到Unity 支付成功的回调。
2、Unity IAP 测试不能连接Unity Editor测试,必须要打包才行。
- 连接Unity Editor测试时:随便传入字符串都可以初始化成功,并且每次也都会购买成功,并且没有正常的支付流程,比如输入沙盒账号,提示购买成功等
3、商品1支付完成,但没有调用结束交易接口;此时无法继续购买商品1,但不影响购买其他商品。
4、Unity IAP 没有像iOS IAP一样提供我们类似交易队列的东西,我们只能被动接受Unity IAP给我们的支付回调。
- 在应用中,可以多次重复初始化Unity IAP服务;每次初始化IAP服务时,也可以携带不同的商品ID。但建议一次性初始化应用中所有的商品ID,因为Unity IAP的只会回调在 初始化IAP服务时携带的商品。(比如商品1已支付,但未消单。此时再次初始化IAP时不包含商品1,那么不会有商品1支付成功的回调)
- 每次初始化Unity IAP服务,Unity内部都会检查是否有已支付但没结束的交易。如果有,就会按照交易创建的顺序,依次返回支付成功回调,每次凭证分别包含着所有被卡着的交易。(比如商品1支付完成,未调用结束交易接口;商品2支付完成,两个支付卡住时,还未消单。再次初始化IAP服务,unity会按照交易创建的顺序,依次返回成功回调,且两个凭证分别包含着两笔交易。)
- 建议初始化Unity IAP服务的时机尽可能的早,这样才能尽可能早收到成功和失败的回调,避免用户卡单。
5、如果使用无效的包名,Unity IAP服务就会初始化失败。
6、如果初始化IAP所携带的商品ID都不是对应包名的,IAP服务就会初始化失败,错误原因:NoProductsAvailable;
7、如果支付时的商品ID,没有被包含在初始化IAP服务的参数中,会支付报错
8、测试Unity IAP时,和iOS IAP流程基本一致。