【Unity】【Mac】Unity-Mac平台内购开发

avatar
作者
筋斗云
阅读量:0

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 版本对应的开发文档,选择你所需要的即可:

image-20240315114949991.png
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

image.png

2、打开IAP Service服务

image.png

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

image-20240320140348977.png
image-20240320140330759.png

image-20240320140341037.png

4.参考链接

Unity 之 接入IOS内购过程解析

构建 Mac App Store 应用之必备知识

Unity 之 上传Mac App Store过程详解

macOS开发 证书等配置/打包后导出及上架

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!