Unity 之 Mac App Store 内购过程解析(购买非消耗道具 | 恢复购买 | 支付验证)

Posted 陈言必行

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity 之 Mac App Store 内购过程解析(购买非消耗道具 | 恢复购买 | 支付验证)相关的知识,希望对你有一定的参考价值。

Unity 之 Mac App Store 内购过程解析(恢复购买)

准备工作

  1. 苹果后台设置
  2. 创建工程导入内购插件

需要详细步骤请查看:
Unity 之 接入IOS内购过程解析

Unity内购官方文档

Mac支付和ios逻辑基本一致,这是我之前做IOS内购时的思维导图,可以看下,先有个概念:


一,具体实现

1.1 场景搭建

创建四个按钮,分别为购买道具清空日志购买非消耗道具恢复购买 ;为了方便查看日志,我还创建了一个ScrollView组件下面放了一个Text接受日志输出。

创建完成效果如下:


1.2 代码实现

完整代码如下:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.UI;

/// <summary>
/// IAP管理类
/// </summary>
public class IAPManagerTest : MonoBehaviour, IStoreListener

    public Text riZhiText;
    
    /// <summary>
    /// 需要换成对应游戏后台的key
    /// </summary>
    private string[] goodsList = new string[]
    
        "com.Czhenya.zuan10",
    ;
    
    /// <summary>
    /// 非消耗型道具 -- 去除广告的id
    /// </summary>
    private string removedsId = "com.Czhenya.delad";

    private bool isRestore = false;
    
    // 控制器
    private IStoreController controller;

    // 苹果扩展
    private IAppleExtensions appleExtensions;

    // 谷歌商店扩展
    private IGooglePlayStoreExtensions googlePlayStoreExtensions;
    
    private static IExtensionProvider extensionProvider;

    // 是否可以发起购买
    private bool isCanOnClickBubBtn = false;

    void Start()
    
        Application.targetFrameRate = 60;
        Init();
    

    /// <summary>
    /// 初始化
    /// </summary>
    private void Init()
    
        // 没有网络,IAP会一直初始化
        if (Application.internetReachability == NetworkReachability.NotReachable)
        
            Debug.Log("----- 用户没有连接网络 IAP不可用 ------");
            riZhiText.text += "----- 用户没有连接网络 IAP不可用 ------\\n";
        

        var module = StandardPurchasingModule.Instance();
        ConfigurationBuilder builder = ConfigurationBuilder.Instance(module);
        // builder.AddProduct("商品id1", ProductType.Consumable); 
        // ProductType :和后台说明对应
        // consumable:可消费的,如游戏中的金币,用完还可以再购买。
        // non-consumable:不可销毁的,一次购买,永久生效。比如去广告,解锁游戏关卡,这种商品只能购买一次。
        // subscription:订阅的,这种一般用于新闻、杂志、或者app里面的月卡。可以按月或者按年收费。
        for (int i = 0; i < goodsList.Length; i++)
        
            builder.AddProduct(goodsList[i], ProductType.Consumable);
        
        
        // 不可销毁的,一次购买,永久生效。比如去广告,解锁游戏关卡,这种商品只能购买一次。
        builder.AddProduct(removedsId, ProductType.NonConsumable);

        riZhiText.text += "----- 开始初始化... ------\\n";
        // 开始初始化
        UnityPurchasing.Initialize(this, builder);
    

    /// <summary>
    /// 初始化成功 -- 接口函数
    /// </summary>
    /// <param name="controller"></param>
    /// <param name="extensions"></param>
    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    
        Debug.Log("【Unity IAP】初始化成功 IAP initialize success");
        riZhiText.text += "【Unity IAP】初始化成功 IAP initialize success\\n";
        isCanOnClickBubBtn = true;
        this.controller = controller;

        // 回调赋值
        extensionProvider = extensions;
        appleExtensions = extensions.GetExtension<IAppleExtensions>();
        googlePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();

        //登记 购买延迟 监听器
        appleExtensions.RegisterPurchaseDeferredListener(OnDeferred);
    

    //购买延迟提示
    private void OnDeferred(Product item)
    
        Debug.Log("【Unity IAP】 网速慢.................");
        riZhiText.text += "【Unity IAP】 网速慢.................\\n";
    

    /// <summary>
    /// 初始化失败回调 -- 接口函数
    /// </summary>
    /// <param name="error"></param>
    public void OnInitializeFailed(InitializationFailureReason error)
    
        Debug.LogError("【Unity IAP】初始化失败 OnInitializeFailed, reason:" + error.ToString());
        riZhiText.text += "【Unity IAP】初始化失败 OnInitializeFailed, reason:" + error.ToString() + "\\n";
    

    /// <summary>
    /// 购买失败回调 -- 接口函数
    /// </summary>
    /// <param name="i"></param>
    /// <param name="p"></param>
    public void OnPurchaseFailed(Product i, PurchaseFailureReason p)
    
        Debug.LogError("【Unity IAP】购买失败 OnPurchaseFailed,reason:" + p.ToString());
        riZhiText.text += "【Unity IAP】购买失败 OnPurchaseFailed,reason:" + p.ToString() + "\\n";
        if (this.onPurchaseFailed != null)
        
            this.onPurchaseFailed();
            this.onPurchaseFailed = null;
        
    

    /// <summary>
    /// 购买成功回调 -- 接口函数
    /// </summary>
    /// <param name="e"></param>
    /// <returns></returns>
    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
    
        Debug.Log("【Unity IAP】购买成功 purchase finished, apple return receipt:" + e.purchasedProduct.receipt);
        //riZhiText.text += "【Unity IAP】购买成功 purchase finished, apple return receipt:" + e.purchasedProduct.receipt + "\\n";
        riZhiText.text += "【Unity IAP】购买成功 e.purchasedProduct.definition.id:" + e.purchasedProduct.definition.id + "\\n";
        riZhiText.text += "【Unity IAP】恢复购买成功 isRestore: " + isRestore + "\\n";

        if (isRestore) // 恢复购买
        
            Debug.Log("恢复购买成功 isRestore " + isRestore);
   
            // 判断是否是去除广告id
            if (removedsId.Equals(e.purchasedProduct.definition.id))
            
                Debug.Log("恢复购买成功");
                // todo... 恢复成功回调
                isRestore = false;
            
            else
            
                onPurchaseFailed?.Invoke();
            

            return PurchaseProcessingResult.Complete;
        
        
        if (this.onPurchaseSuccess != null)
        
            this.onPurchaseSuccess(e.purchasedProduct.receipt);
            this.onPurchaseSuccess = null;
        

        return PurchaseProcessingResult.Complete;
    

    /// <summary>
    /// 支付失败回调
    /// </summary>
    private Action onPurchaseFailed;

    /// <summary>
    /// 支付成功回调
    /// </summary>
    private Action<string> onPurchaseSuccess;

    /// <summary>
    /// 购买产品
    /// </summary>
    /// <param name="productId">产品ID</param>
    /// <param name="onFailed">失败回调</param>
    /// <param name="onSuccess">成功回调</param>
    public void PurchaseProduct(string productId, Action onFailed, Action<string> onSuccess)
    
        this.onPurchaseFailed = onFailed;
        this.onPurchaseSuccess = onSuccess;

        if (controller != null)
        
            var product = controller.products.WithID(productId);
            if (product != null && product.availableToPurchase)
            
                Debug.Log("【Unity IAP】开始购买");
                riZhiText.text += "【Unity IAP】开始购买... \\n";

                controller.InitiatePurchase(productId);
            
            else
            
                Debug.LogError("【Unity IAP】失败回调 no product with productId:" + productId);
                riZhiText.text += "【Unity IAP】失败回调 no product with productId:" + productId + " \\n";
                if (this.onPurchaseFailed != null)
                
                    this.onPurchaseFailed();
                
            
        
        else
        
            Debug.LogError("【Unity IAP】失败回调 controller is null,can not do purchase");
            riZhiText.text += "Unity IAP】失败回调 controller is null,can not do purchase \\n";
            if (this.onPurchaseFailed != null)
            
                this.onPurchaseFailed();
            
        
    

    /// <summary>
    /// 发起购买函数  -- 商城按钮监听
    /// </summary>
    /// <param name="i"></param>
    public void OnClickPurchase(int i)
    
        // 正式项目时需限制 -- 不允许多次点击 

        Debug.Log("【Unity IAP】发起购买函数 " + Application.internetReachability);
        riZhiText.text += "【Unity IAP】发起购买函数  "+Application.internetReachability+" \\n";
        if (Application.internetReachability == NetworkReachability.NotReachable)
        
            Debug.Log("【Unity IAP】用户没网... ");
            return;
        

        PurchaseProduct(goodsList[0], OnBuyFailed, OnBuySuccess);
    

    #region 购买回复非消耗道具

    /// <summary>
    /// 购买非消耗道具 -- 商城按钮监听
    /// </summary>
    public void OnClickRemoved()
    
        // 正式项目时需限制 -- 不允许多次点击 

        Debug.Log("【Unity IAP】购买一次性道具 " + Application.internetReachability);
        riZhiText.text += "【Unity IAP】购买一次性道具  "+Application.internetReachability+" \\n";
        if (Application.internetReachability == NetworkReachability.NotReachable)
        
            Debug.Log("【Unity IAP】用户没网... ");
            return;
        

        PurchaseProduct(removedsId, OnBuyFailed, OnBuySuccess);
    
    
    /// <summary>
    /// 恢复购买非消耗道具 -- 商城按钮监听
    /// </summary>
    public void OnClickRecover()
    
        // 正式项目时需限制 -- 不允许多次点击 

        Debug.Log("【Unity IAP】恢复购买 " + Application.internetReachability);
        riZhiText.text += "【Unity IAP】恢复购买  "+Application.internetReachability+" \\n";
        if (Application.internetReachability == NetworkReachability.NotReachable)
        
            Debug.Log("【Unity IAP】用户没网... ");
            return;
        

        if (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.OSXPlayer)
        
            Debug.Log("发起恢复请求");
            isRestore = true;
            IAppleExtensions apple = extensionProvider.GetExtension<IAppleExtensions>();
            apple.RestoreTransactions(HandleRestored);
        
        else
        
            Debug.Log("恢复购买失败. 不支持这个平台. 当前平台 = " + Application.platform);
        

    
    
    // 恢复购买之后,会返回一个状态,如果状态为true,
    // 之前购买的非消耗物品都会回调一次购买成功(ProcessPurchase)
    // 然后在这里个回调里面进行处理
    void HandleRestored(bool result)
    
        // 返回一个bool值,如果成功,则会多次调用支付回调,然后根据支付回调中的参数得到商品id,最后做处理(ProcessPurchase)
        Debug.Log("恢复购买继续: " + result + ". 如果没有进一步的消息,则没有可恢复的购买。");
        isRestore = result;
        riZhiText.text += "【Unity IAP】恢复购买继续  " + result + ". 如果没有进一步的消息,则没有可恢复的购买。 \\n";
        if (result)
        
            riZhiText.text += "【Unity IAP】恢复购买成功! \\n";
            Debug.Log("恢复购买成功!");
        
        else
        
            riZhiText.text += "【Unity IAP】恢复购买失败! \\n";
            Debug.Log("恢复购买失败!");
        
        // todo...回调处理
    

    #endregion
    
    /// <summary>
    /// 购买失败回调
    /// </summary>
    void OnBuyFailed()
    
        Debug.Log("【Unity IAP】购买失败回调 OnBuyFailed...");
        riZhiText.text += "【Unity IAP】购买失败回调 OnBuyFailed... \\n";
    

    /// <summary>
    /// 购买成功回调
    /// </summary>
    /// <param name="str"></param>
    void OnBuySuccess(string str)
    
        Debug.Log("【Unity IAP】购买成功回调 OnBuySuccess..." + str);
        riZhiText.text += "【Unity IAP】购买成功回调 OnBuySuccess... \\n";
        riZhiText.text += "【Unity IAP】购买成功...收据: " + str + " \\n";
        //会得到下面这样一个字符串
        //"Store":"AppleAppStore",
        //"TransactionID":"1000000845663422",
        //"Payload":"MIIT8QYJKoZIhvcNAQcCoIIT4jCCE94CAQExBBMMIIBa ... 还有N多 ..."
    

    public void ClearRiZhi()
    
        riZhiText.text = "清空数据\\n";
    

PS:此代码为上图使用的测试代码,按钮点击监听赋值,在Inspector面板下拖拽赋值。正式使用时可自行删除注释或者点击获取源码


1.3 打包设置

将包名修改为与后台一致,其他属性默认即可。若需要更多设置,可参考:Unity 之 打包参数 – Player面板属性详解


二,打包测试

2.1 实现步骤说明

Mac内购流程打包步骤

  1. 使用正式包名
    和苹果后台创建的对应上,直接在Unity里面设置好。
  2. 签名app并打包为pkg
    若在Unity中没有设置正确包名,也可以直接在打包处理的app,右键显示包内容,找到信息.plist文件并将CFBundleIdentifier字符串更新为应用程序的包名。
    在2.2中还有详细的签名和导出pkg的步骤
  3. 安装pkg并调用初始化内购项
    要正确安装软件包,请删除未打包的。运行新创建的软件包并安装它之前的应用程序文件。
    在3.1中有测试步骤实现过程
  4. 调用购买并尝试购买查看返回数据
    测试结果和支付验证

2.2 Mac签名命令

签名需要两个证书和一个签名文件,若之前都没搞过,则可以参考:Unity 之 上传Mac App Store过程详解
文章中有详细获取证书步骤和签名配置所需文件。

  1. 设置权限
chmod -R a+xr "/Users/Czhenya/Desktop/Mac/你的.app"
  1. 文件签名
codesign -o runtime -f --deep -s '3rd Party Mac Developer Application: 证书.' --entitlements "/Users/Czhenya/Desktop/App.entitlements" "/Users/Czhenya/Desktop/Mac/你的.app"
  1. 打包pkg
productbuild --component /Users/Czhenya/Desktop/Mac/你的.app /Applications --sign "3rd Party Mac Developer Installer: 证书." /Users/Czhenya/Desktop/Mac/你的.pkg

三,示例演示

3.1 购买商品

  1. 商品初始化成功:

  2. 输入沙盒账号:(首次使用会有双重认证之类的确保身份安全,可选择跳过或按照提示操作即可)

  3. 若是第一次登录,需要确认Apple ID 安全,点击继续:

  4. 若出现“保护您的账号”提示,选择不升级即可:

  5. 最后终于到了支付购买界面了,点击购买就可了:

  6. 购买完成后,会弹出操作完成提示,点击“好“即可触发支付成功回调(沙盒会稍微慢一点)

  7. 支付成功回调:

  8. 取消支付回调:


3.2 购买非消耗道具

  1. 初始化成功后,点击购买非消耗道具

3.3 恢复购买

  1. 购买过一次之后,再次购买会购买失败,这时需要点击恢复购买,执行恢复购买逻辑

四,支付验证

若是单机游戏无需服务器进行支付验证,则按照成功回调发放奖励跳过此步骤即可。若需要服务器验证,则将支付成功的Payload传到服务器,获取验证结果后发放奖励或提示支付失败。

4.1 验证返回数据

服务端验证返回数据
iOS发起票据验证请求后,通过处理AppStore返回数据来验单。服务验证需要注意的地方:不同iOS版本的返回数据不同,服务端验证方式也不同。

  1. iOS7及以上获取的票据返回数据:

    receipt =  
        "adam_id" = 0,
        "app_item_id" = 0,
        "application_version" = 1,
        "bundle_id" = "com.Czhenya",
        "download_id" = 0,
        "in_app" = 
            
                "is_trial_period" = false,
                "original_purchase_date" = "2022-10-24 01:00:00 Etc/GMT",
                "original_purchase_date_ms" = 1483203661000,
                "original_purchase_date_pst" = "2022-10-24 01:00:00 America/Los_Angeles",
                "original_transaction_id" = 1000000000000001,
                "product_id" = "com.Czhenya.zuan10",
                "purchase_date" = "2022-10-24 01:00:00 Etc/GMT",
                "purchase_date_ms" = 1483203661000,
                "purchase_date_pst" = "2022-10-24 01:00:00 America/Los_Angeles",
                "transaction_id" = 1000000000000001
            
        ,
        "receipt_type" = "ProductionSandbox",
        "request_date" = "2022-10-24 01:00:00 Etc/GMT",
        "request_date_ms" = 1483203661000,
        "request_date_pst" = "2022-10-24 01:00:00 America/Los_Angeles",
        "version_external_identifier" = 0,
    ,
    status = 0

  1. iOS7以下获取的票据返回数据(不包括iOS7):

    receipt = 
        "bid" = "com.Czhenya",
        "bvrs" = 1,
        "item_id" = 573837050,
        "original_purchase_date" = 以上是关于Unity 之 Mac App Store 内购过程解析(购买非消耗道具 | 恢复购买 | 支付验证)的主要内容,如果未能解决你的问题,请参考以下文章

Unity 之 上传Mac App Store过程详解

unity上传app store遇到的一些问题

Unity 之 接入IOS内购过程解析文末源码

Unity接入OneStore内购

怎么跨区内购苹果

苹果“屈服”了?App Store 竟允许第三方支付!