当前位置:   article > 正文

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

mac unity开发

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
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229

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

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号