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流程基本一致。