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

2 篇文章 0 订阅
文章详细介绍了在MacOS环境下使用Unity进行In-AppPurchasing(IAP)开发的过程,包括配置、获取文档、支付代码实现以及遇到的问题与解决方案,如失去焦点后的回调、UnityEditor测试、交易处理和初始化商品ID策略等。
摘要由CSDN通过智能技术生成

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开发 证书等配置/打包后导出及上架

  • 17
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值