精华内容
下载资源
问答
  • 主要是Java对接iOS内购,回调验证相关的方法, 真机测试的时候,一定要退出原来的账号,才能用沙盒测试账号,二次验证,请注意区分宏, 测试用沙盒验证,App Store审核的时候也使用的是沙盒购买,所以验证购买凭证的...
  • iOS内购与apple pay

    2021-01-11 16:32:12
    首先得分清楚 内购和Apple Pay。 一、Apple Pay:是一种支付方式,跟支付宝、微信支付是类似的,这里就不详细介绍了。 二、内购:只要在iPhone App上购买的不是实物产品(也就是虚拟产品如qq币、鱼翅、电子书......) ...
  • Unity接入IOS内购demo 从Unity导出Xcode项目 恢复购买 配置商品和测试账号
  • 内有微信app支付和ios内购支付的demo,语言为php,如果不足欢迎提建议,谢谢,希望能帮助大家。
  • IAP支付(IOS内购

    热门讨论 2015-07-31 17:10:13
    IOS内购(IAP支付)的C++封装,适用于Cocos2d-x开发。 教程地址:http://blog.csdn.net/ldpjay/article/details/46459509
  • iOS内购实现及测试CheckList.免费+应用内购买的模式已经被证明了是最有效的盈利模式,所以实现内购功能可能是很多开发者必做的工作和必备的技能了。但是鉴于内购这块坑不算少,另外因为sandbox测试所需要特定的配置...
  • iOS内购功能demo

    2017-07-19 17:13:08
    iOS内购功能简易实现
  • iOS内购笔记

    2016-11-30 16:17:00
    iOS内购
  • Unity 之 接入IOS内购过程解析【文末源码】

    千次阅读 多人点赞 2021-09-08 09:59:58
    看完此文章你可以了解IOS内购接入全过程,可以学习到Unity从零接入内购功能

    前言

    看完此文章你可以了解IOS内购接入全过程,可以学习到Unity从零接入内购功能。另外此博文和文末源码没有涉及到掉单补单部分逻辑。

    需要准备

    • 一台mac系统机器
    • 苹果开发者账号
    • Unity2019.4.x (不同版本,3步骤略有不同)
    • Xcode (我的版本12.5)

    内购导图

    内购导图

    一,效果展示


    二,苹果后台

    PS:若公司已有运营人员在后台操作过了,可以跳过此步骤。注意测试机上还是需要登陆沙箱账号奥。

    2.1 注册应用程序

    1. 首先打开苹果开发者官网:https://developer.apple.com/

    2. 点击登陆并点“Account”,在Apple Developer Center中,导航到相应的标识符部分:

    1. 添加新的 App ID 以创建与 Apple 的基本应用程序实体。

      • 注意:使用显式应用程序 ID。通配符应用 ID (com.example.*) 不能用于使用应用内购买的应用。

      • 注意:在开发者中心创建 App ID 后,即可在 iTunes Connect 中使用它。

    1. 导航到iTunes Connect并创建一个应用程序,以与游戏建立商店关系:

    1. 使用新创建的 App ID 作为应用的 Bundle ID:

    2.2 添加应用内购买

    1. 选择功能并使用加号 ("+") 按钮添加新的应用内购买:

    2. 选择产品类型:

    1. 指定产品标识符,并根据要求填写其他字段。
    • 注意:此处的“产品 ID”与游戏源代码中使用的标识符相同,通过AddProduct()或AddProducts()添加到Unity IAP ConfigurationBuilder实例。

    2.3 测试IAP

    1. 使用iTunes Connect创建沙盒测试器以在您的测试设备的iTunes帐户上使用。为此,请导航至iTunes Connect > Users and Roles,然后选择加号 ("+") 按钮。

    PS:详情可查看AppleSandbox Tester文档,。

    1. Xcode项目配置
      Xcode 项目中Bundle IdentitifierTeamiTunes Connect 中使用的一致
      PS:Unity中的包名也应该保持一致

    1. 在测试设备登陆沙箱测试账号

    三,下载IAP包

    3.1 下载Package

    打开Windows -> Package Manager 下载 In App Purchasing

    3.2 打开Srever配置

    1. 打开服务窗口,在服务窗口中查找和启用应用内购买

    2. 选择项目ID(当前登录的账号)

    3. 启用In-APP Purchasing (有的时候切换慢,需要等一会)

    4. 回答问题
      问:这款应用主要面向13岁以下的儿童(是就勾选,不是不勾选)

    5. 有个报错
      我没有解决也没有影响,需要解决的话按照下面的提示操作一下


    四,代码逻辑

    4.1 逻辑分析

    1. 实现IStoreListener接口,接口提供四个回调函数,分别是初始化成功、失败,购买成功、失败;

    2. 编写初始化逻辑,完善初始化成功、失败回调接口函数;

    3. 编写调用购买逻辑,完善购买成功、失败回调接口函数;

    4. 实际开发中需要限制,购买按钮只被点击一次。

    代码结构就是这样了,详细解释代码注释已经写得很清楚了,这里不再赘述。

    使用时将代码挂载到场景即可进行初始化,然后创建Button监听代码中的OnClickPurchase方法即可打包测试。

    PS:注意需要将goodsList数组中的key换成你后台申请的

    4.2 示例源码

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Purchasing;
    
    /// <summary>
    /// IAP管理类
    /// </summary>
    public class IAPManager : MonoBehaviour, IStoreListener
    {
        /// <summary>
        /// 需要换成对应游戏后台的key
        /// </summary>
        private string[] goodsList = new string[]
        {
            "com.czhenya.gold.1",
            "com.czhenya.gold.2",
            "com.czhenya.gold.3"
        };
    
        // 控制器
        private IStoreController controller;
    
        // 苹果扩展
        private IAppleExtensions appleExtensions;
    
        // 谷歌商店扩展
        private IGooglePlayStoreExtensions googlePlayStoreExtensions;
    
        // 是否可以发起购买
        private bool isCanOnClickBubBtn = false;
    
        void Start()
        {
            Init();
        }
    
        /// <summary>
        /// 初始化
        /// </summary>
        private void Init()
        {
            // 没有网络,IAP会一直初始化
            if (Application.internetReachability == NetworkReachability.NotReachable)
            {
                Debug.Log("----- 用户没有连接网络 IAP不可用 ------");
            }
    
            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);
            }
    
            // 开始初始化
            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");
            isCanOnClickBubBtn = true;
            this.controller = controller;
    
            // 回调赋值
            this.appleExtensions = extensions.GetExtension<IAppleExtensions>();
            this.googlePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();
    
            //登记 购买延迟 监听器
            appleExtensions.RegisterPurchaseDeferredListener(OnDeferred);
        }
    
        //购买延迟提示
        private void OnDeferred(Product item)
        {
            Debug.Log("【Unity IAP】 网速慢.................");
        }
    
        /// <summary>
        /// 初始化失败回调 -- 接口函数
        /// </summary>
        /// <param name="error"></param>
        public void OnInitializeFailed(InitializationFailureReason error)
        {
            Debug.LogError("【Unity IAP】初始化失败 OnInitializeFailed, reason:" + error.ToString());
        }
    
        /// <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());
            if (this.onPurchaseFailed != null)
            {
                this.onPurchaseFailed();
                this.onPurchaseFailed = null;
            }
        }
    
        /// <summary>
        /// 购买成功回调 -- 接口函数
        /// </summary>
        /// <param name="e"></param>
        /// <returns></returns>
        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
        {
            Debug.LogError("【Unity IAP】购买过程 purchase finished, apple return receipt:" + e.purchasedProduct.receipt);
    
            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】开始购买");
                    controller.InitiatePurchase(productId);
                }
                else
                {
                    Debug.LogError("【Unity IAP】失败回调 no product with productId:" + productId);
                    if (this.onPurchaseFailed != null)
                    {
                        this.onPurchaseFailed();
                    }
                }
            }
            else
            {
                Debug.LogError("【Unity IAP】失败回调 controller is null,can not do purchase");
                if (this.onPurchaseFailed != null)
                {
                    this.onPurchaseFailed();
                }
            }
        }
    
        /// <summary>
        /// 发起购买函数  -- 商城按钮监听
        /// </summary>
        /// <param name="i"></param>
        public void OnClickPurchase(int i)
        {
            // 正式项目时需限制 -- 不允许多次点击 
    
            Debug.Log("【Unity IAP】发起购买函数 " + Application.internetReachability);
            if (Application.internetReachability == NetworkReachability.NotReachable)
            {
                Debug.Log("【Unity IAP】用户没网... ");
                return;
            }
    
            PurchaseProduct(goodsList[0], OnBuyFailed, OnBuySuccess);
        }
    
        /// <summary>
        /// 购买失败回调
        /// </summary>
        void OnBuyFailed()
        {
            Debug.Log("【Unity IAP】购买失败回调 OnBuyFailed...");
        }
    
        /// <summary>
        /// 购买成功回调
        /// </summary>
        /// <param name="str"></param>
        void OnBuySuccess(string str)
        {
            Debug.Log("【Unity IAP】购买成功回调 OnBuySuccess..." + str);
            //会得到下面这样一个字符串
            //{"Store":"AppleAppStore",
            //"TransactionID":"1000000845663422",
            //"Payload":"MIIT8QYJKoZIhvcNAQcCoIIT4jCCE94CAQExBBMMIIBa ... 还有N多 ..."}
        }
    }
    

    五,打包测试

    代码配置和手动配置选择一个习惯用的方式即可。

    5.1 代码配置

    由于内购需要系统库StoreKit.frameworkiAd.framework。为了不每次打包Xcode时都手动添加,所以创建打包配置代码。(复制下面文件,放到Editor文件夹下)

    using System.IO;
    using UnityEditor;
    using UnityEngine;
    #if UNITY_IOS
    using UnityEditor.Callbacks;
    using UnityEditor.iOS.Xcode;
    #endif
    
    /// <summary>
    /// 打包自动配置文件
    /// </summary>
    public class CZYConfigEditor
    {
    #if UNITY_IOS
        [PostProcessBuildAttribute(100)]
        public static void onPostProcessBuild(BuildTarget target, string targetPath)
        {
            if (target != BuildTarget.iOS)
            {
                return;
            }
    
            string projPath = PBXProject.GetPBXProjectPath(targetPath);
            PBXProject proj = new PBXProject();
            proj.ReadFromString(File.ReadAllText(projPath));
            string unityTarget = proj.GetUnityFrameworkTargetGuid();
    
            #region 系统依赖库
    
            proj.AddFrameworkToProject(unityTarget, "StoreKit.framework", false);
            proj.AddFrameworkToProject(unityTarget, "iAd.framework", false);
    
            #endregion
    
            string content = proj.WriteToString();
            File.WriteAllText(projPath, content);
        }
    #endif
    }
    

    5.2 手动配置

    不写上面代码的话,打包出Xcode工程后,需要手动添加StoreKit.frameworkiAd.framework

    然后正常打包进行测试~ 即可完成开篇效果。


    六,问题汇总

    6.1 示例日志

    • IAP初始化成功日志:

    • 购买成功回调日志:

    【Unity IAP】购买成功回调 OnBuySuccess…{“Store”:“AppleAppStore”,“TransactionID”:“1000000866663121”,“Payload”:“MIIT8QYJKoZIhvcNAQcCo
    …中间省略N多行…
    jSYLAk”}
    System.Action`1:Invoke(T)
    IAPMgr:ProcessPurchase(PurchaseEventArgs)
    UnityEngine.Purchasing.PurchasingManager:ProcessPurchaseIfNew(Product)
    UnityEngine.Purchasing.JSONStore:OnPurchaseSucceeded(String, String, String)
    System.Action:Invoke()
    UnityEngine.Purchasing.Extension.UnityUtil:Update()

    6.2 注意事项

    • 真机测试的时候,一定要退出原来的账号(app store 登录的账号退出),才能用沙盒测试账号。
    • 请务必使用真机来测试,一切以真机为准。
    • 项目的Bundle identifier需要与您申请AppID时填写的bundleID一致,不然会无法请求到商品信息。
    • 沙盒环境测试appStore内购流程的时候,请使用没越狱的设备。
    • 沙盒的测试账号和你请求商品信息没有关系。请求商品信息的流程是,你在后台配置好了内购商品,并且将其添加到了需要集成内购功能的App中,然后你请求商品。请求到商品后的流程是这样的,苹果系统会自动弹出登录框让你登录账号。然后根据提示操作进行购买,这里的账号就是你配置的沙盒测试账号。

    6.3 参考链接

    官方文档 Unity IAP

    官方手册 Unity IAP

    6.4 文末源码

    其实源码以及步骤都在上面分享过了,若还有什么不明白的,可以点击下面链接下载,积分不够的童鞋可以私信我哦~

    源码链接


    展开全文
  • iOS内购原因审核被拒

    2021-03-03 14:37:25
    【uniapp】打包成iOS内购原因审核被拒,自己在沙箱环境测试正常支付,审核人员不行;被拒原因如下图,求助200-500元感谢。 <p><img alt="" height="848" src=...
  • iOS内购二:购买和恢复

    千次阅读 2020-09-09 11:28:41
    iOS内购二:购买和恢复 购买 构建一个SKPayment对象,传递SKProduct。SKPayment被创建后,就会将其加入到SKPaymentQueue队列中 然后用户会授权,payment是异步,所以需要一个observer,apple建议observer添加都的...

    iOS内购二:购买和恢复

    购买

    构建一个SKPayment对象,传递SKProductSKPayment被创建后,就会将其加入到SKPaymentQueue队列中

    然后用户会授权,payment是异步,所以需要一个observer,apple建议observer添加都的AppDelegate中

    购买队列
    支付之后,app store会返回一个transaction,transaction包含receipt(被加密签名,可以获取购买的信息)和 state
    最后,还需要让apple知道你已经完成处理这个transaction,如果不这么做的话,apple会一致给你发送这个transaction

    transaction
    如下所示,在AppDelegate中添加观察者:

    	func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
        SKPaymentQueue.default().add(self)
        
    		return true
    	}
    

    实现SKPaymentTransactionObserver中的方法,监听状态

    extension AppDelegate: SKPaymentTransactionObserver {
      func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        
        for transaction in transactions {
          switch transaction.transactionState {
          case .purchased:
            completeTransaction(transaction)
          case .failed:
            failedTransaction(transaction)
          default:
            print("Unhandled transaction state")
          }
          
        }
        
      }
    

    恢复

    如果用户在一个新设备上登录,希望要恢复原来的购买项目
    需要记住的是,不是所有的都可以恢复,Consumable 和 Non-Renewing Subscriptions 是不能恢复的
    恢复,需要调用pagyment queue的restoreCompletedTransactions()。恢复purchase会为旧的transaction创建新的transaction

    恢复购买流程

      //恢复购买
      public func restorePurchases() {
        SKPaymentQueue.default().restoreCompletedTransactions()
      }
    
    extension AppDelegate: SKPaymentTransactionObserver {
      func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        
        for transaction in transactions {
          switch transaction.transactionState {
          case .purchased, .restored: //购买 or 恢复
            completeTransaction(transaction)
          case .failed:
            failedTransaction(transaction)
          default:
            print("Unhandled transaction state")
          }
          
        }
        
      }
    

    non-renewing subscription

    非续期订阅需要管理期限,当其过期后,需要手动再续期。iTunes并不管理,需要自己管理
    可使用iCloud来管理,但如果app是跨平台的,使用iCloud并不是一个好的选择,iCloud限制在iOS设备上

    auto-renewable subscription

    自动续期订阅与非自动续期订阅非常类似,但也有一些区别
    1.首先需要创建subscription group

    可参考官方文档:自动续期订阅

    您提供的每个订阅都必须分配到一个订阅群组。一个订阅群组包含多个具有不同访问级别、价格和持续时间的订阅,便于用户选择最符合自己需求的选项。由于用户从一个群组中一次只能购买一个订阅,因此对于大多数 app 而言,最佳做法是只创建一个群组,这样可以防止用户意外购买多个订阅。

    升级。用户购买服务级别高于当前订阅的订阅。他们的订阅服务会立即升级,并会获得原始订阅的按比例退款。如果您希望用户能够立即访问更多内容或功能,请为该订阅指定较高排名,将其作为升级选项。

    降级。用户选择服务级别低于当前订阅的订阅。订阅会继续保持不变,直到下一个续订日期,然后以较低级别和价格续订。

    跨级。用户切换到相同级别的新订阅。如果两个订阅的持续时间相同,新订阅会立即生效。如果持续时间不同,新订阅会在下一个续订日期生效。

    其它可参考项:

    展开全文
  • 最近我们公司丢单率上涨,尤其是10月份比9月份来说丢单率翻了3倍,和一些同行交流了一下,发现他们也是丢单量增加,初步推断可能是苹果iOS12的原因,某些情况下会有用户内购成功后,却返回的是订单失败,错误类型为...

    ---------------------------2018.10.16更新---------------------------

    最近我们公司丢单率上涨,尤其是10月份比9月份来说丢单率翻了3倍,和一些同行交流了一下,发现他们也是丢单量增加,初步推断可能是苹果iOS12的原因,某些情况下会有用户内购成功后,却返回的是订单失败,错误类型为SKErrorUnknown。目前客户端好像没办法去解决。如果有小伙伴和我一样也遇到过相同的问题话,请私信我下,我们都多互相交流一下。

    ---------------------------2018.10.16更新---------------------------

    ---------------------------以下为正文---------------------------

    iOS内购开发大家一定不陌生,网上类似的文章能搜出千八百篇。大部分都是围绕着如何实现?如何防止漏单丢单说明的。很少有提及到越狱的,即使偶尔有一两篇说越狱,也是简单的三言两语说 为了安全,我们直接屏蔽了越狱手机的内购功能。巴拉巴拉... 以前我也是这么想的,直到上个周末发现我们的内购被破解了...才有了这篇文章。本篇文章就是来讲述越狱下的内购如何防止被破解。

    首先我们先简单理一下整个内购的核心流程:

    1. 客户端发起支付订单

    2. 客户端监听购买结果

    3. 苹果回调订单购买成功时,客户端把苹果给的receipt_data和一些订单信息上报给服务器

    4. 后台服务器拿receipt_data向苹果服务器校验

    5. 苹果服务器向返回status结果,含义如下,其中为0时表示成功。

    • 21000 App Store无法读取你提供的JSON数据

    • 21002 收据数据不符合格式

    • 21003 收据无法被验证

    • 21004 你提供的共享密钥和账户的共享密钥不一致

    • 21005 收据服务器当前不可用

    • 21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中

    • 21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证

    • 21008 收据信息是产品环境中使用,但却被发送到测试环境中验证

    6.服务器发现订单校验成功后,会把这笔订单存起来,receipt_data用MD5值映射下,保存到数据库,防止同一笔订单,多次发放内购商品。

    以上应该是主流的校验流程。当然客户端其中会插一些丢单漏单的逻辑校验,因为那些跟本篇文章无关,所以不在此展开。

    从上面的流程可以看出,整个内购的核心其实就是receipt_data。苹果回调给客户端,客户端上报给服务器,服务器拿到后去向苹果服务器校验,苹果服务器再返回给我们服务器订单结果。其实严格来说,整个流程是没问题的。整个的漏洞是在最后一步上,【苹果服务器再返回给我们服务器订单结果】。receipt_data在越狱环境下是可以被插件伪造的,后台向苹果验证时,居然还能验证通过。是的,你没看错,苹果这里有个贼鸡儿坑的地方。这是最坑最坑的地方,伪造的receipt_data苹果校验也返回支付成功

    如何解决?我们先来看下越狱订单和正常订单对比

    越狱订单receipt_data向苹果服务器校验后如下:

    {
        "status": 0, 
        "environment": "Production", 
        "receipt": {
            "receipt_type": "Production", 
            "adam_id": 1377028992, 
            "app_item_id": 1377028992, 
            "bundle_id": "*******【敏感信息不给看】*******", 
            "application_version": "3", 
            "download_id": 80042231041057, 
            "version_external_identifier": 827853261, 
            "receipt_creation_date": "2018-07-23 07:30:45 Etc/GMT", 
            "receipt_creation_date_ms": "1532331045000", 
            "receipt_creation_date_pst": "2018-07-23 00:30:45 America/Los_Angeles", 
            "request_date": "2018-07-23 07:33:54 Etc/GMT", 
            "request_date_ms": "1532331234485", 
            "request_date_pst": "2018-07-23 00:33:54 America/Los_Angeles", 
            "original_purchase_date": "2018-07-01 12:16:21 Etc/GMT", 
            "original_purchase_date_ms": "1530447381000", 
            "original_purchase_date_pst": "2018-07-01 05:16:21 America/Los_Angeles", 
            "original_application_version": "3", 
            "in_app": [ ]
        }
    }
    复制代码

    正常订单receipt_data向苹果服务器校验后如下:

    {
       {
        "status": 0, 
        "environment": "Production", 
        "receipt": {
            "receipt_type": "Production", 
            "adam_id": 1377028992, 
            "app_item_id": 1377028992, 
            "bundle_id": "*******【敏感信息不给看】*******", 
            "application_version": "3", 
            "download_id": 36042096097927, 
            "version_external_identifier": 827703432, 
            "receipt_creation_date": "2018-07-10 13:54:27 Etc/GMT", 
            "receipt_creation_date_ms": "1531230867000", 
            "receipt_creation_date_pst": "2018-07-10 06:54:27 America/Los_Angeles", 
            "request_date": "2018-07-23 08:03:27 Etc/GMT", 
            "request_date_ms": "1532333007664", 
            "request_date_pst": "2018-07-23 01:03:27 America/Los_Angeles", 
            "original_purchase_date": "2018-06-13 06:52:13 Etc/GMT", 
            "original_purchase_date_ms": "1528872733000", 
            "original_purchase_date_pst": "2018-06-12 23:52:13 America/Los_Angeles", 
            "original_application_version": "5", 
            "in_app": [
                {
                    "quantity": "1", 
                    "product_id": "*******【敏感信息不给看】*******", 
                    "transaction_id": "160000477610856", 
                    "original_transaction_id": "160000477610856", 
                    "purchase_date": "2018-07-10 13:54:27 Etc/GMT", 
                    "purchase_date_ms": "1531230867000", 
                    "purchase_date_pst": "2018-07-10 06:54:27 America/Los_Angeles", 
                    "original_purchase_date": "2018-07-10 13:54:27 Etc/GMT", 
                    "original_purchase_date_ms": "1531230867000", 
                    "original_purchase_date_pst": "2018-07-10 06:54:27 America/Los_Angeles", 
                    "is_trial_period": "false"
                }
            ]
        }
    }
    复制代码

    看完两笔订单的对比我相信大家可以清楚的知道,越狱订单虽然状态返回是成功的,但是in_app这个参数是空的。大概查了一下。iOS7以下是没有这个in_app参数的,iOS7以上是有的。因为现在App基本支持的起步都是iOS8 iOS9了,iOS7可以不用管了。但这里还有一个问题,就是in_app这个字段并不总是只返回一个,有可能会返回多个,比如下面的这种订单。

    正常订单receipt_data校验后  in_app多个元素时:

    {
        "status":0,
        "environment":"Sandbox",
        "receipt":{
            "receipt_type":"ProductionSandbox",
            "adam_id":0,
            "app_item_id":0,
            "bundle_id":"*******【敏感信息不给看】*******",
            "application_version":"1",
            "download_id":0,
            "version_external_identifier":0,
            "receipt_creation_date":"2018-07-24 04:28:24 Etc/GMT",
            "receipt_creation_date_ms":"1532406504000",
            "receipt_creation_date_pst":"2018-07-23 21:28:24 America/Los_Angeles",
            "request_date":"2018-07-24 04:30:06 Etc/GMT",
            "request_date_ms":"1532406606695",
            "request_date_pst":"2018-07-23 21:30:06 America/Los_Angeles",
            "original_purchase_date":"2013-08-01 07:00:00 Etc/GMT",
            "original_purchase_date_ms":"1375340400000",
            "original_purchase_date_pst":"2013-08-01 00:00:00 America/Los_Angeles",
            "original_application_version":"1.0",
            "in_app":[
                {
                    "quantity":"1",
                    "product_id":"*******【敏感信息不给看】*******",
                    "transaction_id":"1000000398911598",
                    "original_transaction_id":"1000000398911598",
                    "purchase_date":"2018-05-16 03:26:12 Etc/GMT",
                    "purchase_date_ms":"1526441172000",
                    "purchase_date_pst":"2018-05-15 20:26:12 America/Los_Angeles",
                    "original_purchase_date":"2018-05-16 03:26:12 Etc/GMT",
                    "original_purchase_date_ms":"1526441172000",
                    "original_purchase_date_pst":"2018-05-15 20:26:12 America/Los_Angeles",
                    "is_trial_period":"false"
                },
                {
                    "quantity":"1",
                    "product_id":"*******【敏感信息不给看】*******",
                    "transaction_id":"1000000398911640",
                    "original_transaction_id":"1000000398911640",
                    "purchase_date":"2018-05-16 03:26:37 Etc/GMT",
                    "purchase_date_ms":"1526441197000",
                    "purchase_date_pst":"2018-05-15 20:26:37 America/Los_Angeles",
                    "original_purchase_date":"2018-05-16 03:26:37 Etc/GMT",
                    "original_purchase_date_ms":"1526441197000",
                    "original_purchase_date_pst":"2018-05-15 20:26:37 America/Los_Angeles",
                    "is_trial_period":"false"
                },
                {
                    "quantity":"1",
                    "product_id":"*******【敏感信息不给看】*******",
                    "transaction_id":"1000000398911784",
                    "original_transaction_id":"1000000398911784",
                    "purchase_date":"2018-05-16 03:26:50 Etc/GMT",
                    "purchase_date_ms":"1526441210000",
                    "purchase_date_pst":"2018-05-15 20:26:50 America/Los_Angeles",
                    "original_purchase_date":"2018-05-16 03:26:50 Etc/GMT",
                    "original_purchase_date_ms":"1526441210000",
                    "original_purchase_date_pst":"2018-05-15 20:26:50 America/Los_Angeles",
                    "is_trial_period":"false"
                },
                {
                    "quantity":"1",
                    "product_id":"*******【敏感信息不给看】*******",
                    "transaction_id":"1000000398911801",
                    "original_transaction_id":"1000000398911801",
                    "purchase_date":"2018-05-16 03:27:22 Etc/GMT",
                    "purchase_date_ms":"1526441242000",
                    "purchase_date_pst":"2018-05-15 20:27:22 America/Los_Angeles",
                    "original_purchase_date":"2018-05-16 03:27:22 Etc/GMT",
                    "original_purchase_date_ms":"1526441242000",
                    "original_purchase_date_pst":"2018-05-15 20:27:22 America/Los_Angeles",
                    "is_trial_period":"false"
                },
                {
                    "quantity":"1",
                    "product_id":"*******【敏感信息不给看】*******",
                    "transaction_id":"1000000399060767",
                    "original_transaction_id":"1000000399060767",
                    "purchase_date":"2018-05-16 11:10:45 Etc/GMT",
                    "purchase_date_ms":"1526469045000",
                    "purchase_date_pst":"2018-05-16 04:10:45 America/Los_Angeles",
                    "original_purchase_date":"2018-05-16 11:10:45 Etc/GMT",
                    "original_purchase_date_ms":"1526469045000",
                    "original_purchase_date_pst":"2018-05-16 04:10:45 America/Los_Angeles",
                    "is_trial_period":"false"
                },
                {
                    "quantity":"1",
                    "product_id":"*******【敏感信息不给看】*******",
                    "transaction_id":"1000000399061778",
                    "original_transaction_id":"1000000399061778",
                    "purchase_date":"2018-05-16 11:14:52 Etc/GMT",
                    "purchase_date_ms":"1526469292000",
                    "purchase_date_pst":"2018-05-16 04:14:52 America/Los_Angeles",
                    "original_purchase_date":"2018-05-16 11:14:52 Etc/GMT",
                    "original_purchase_date_ms":"1526469292000",
                    "original_purchase_date_pst":"2018-05-16 04:14:52 America/Los_Angeles",
                    "is_trial_period":"false"
                },
                ...
            ]
        }
    }
    复制代码

    综上,整个服务器那边校验逻辑应该是这样的。

    首先客户端必须要给服务器传的三个参数:receipt_data, product_id ,transaction_id

    //该方法为监听内购交易结果的回调
    - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
    transactions 为一个数组 遍历就可以得到 SKPaymentTransaction 对象的元素transaction。然后从transaction里可以取到以下这两个个参数,product_id,transaction_id。另外从沙盒里取到票据信息receipt_data 
    我们先看怎么取到以上的三个参数
    //获取receipt_data
    NSData *data = [NSData dataWithContentsOfFile:[[[NSBundle mainBundle] appStoreReceiptURL] path]];
    NSString * receipt_data = [data base64EncodedStringWithOptions:0];
    //获取product_id
    NSString *product_id = transaction.payment.productIdentifier;
    //获取transaction_id
    NSString * transaction_id = transaction.transactionIdentifier;
    复制代码

    这是我们必须要传给服务器的三个字段。以上三个字段需要做好空值校验,避免崩溃。

    下面我们来解释一下,为什么要给服务器传这三个参数。

    • receipt_data:这个不解释了 大家都懂 不传的话 服务器根本没法校验

    • product_id:这个也不用解释 内购产品编号 你不传的话 服务器不知道你买的哪个订单

    • transaction_id:这个是交易编号,是必须要传的。因为你要是防止越狱下内购被破解就必须要校验in_app这个参数。而这个参数的数组元素有可能为多个,你必须得找到一个唯一标示,才可以区分订单到底是那一笔。

    所以服务器那边逻辑就很清晰了。

    1. 首先判断订单状态是不是成功。

    2. 如果订单状态成功在判断in_app这个字段有没有,没有直接就返回失败了。如果存在的话,遍历整个数组,通过客户端给的transaction_id 来比较,取到相同的订单时,对比一下bundle_id ,product_id 是不是正确的。

    3. 如果以上校验都正确再去查询一下数据库里这笔订单是不是存在,如果存在也是返回失败,避免重复分发内购商品。如果都成功了,再把这笔订单充值进去,给用户分发内购商品。

    注意:一定要告诉后台,不论校验是否成功,只要客户端给服务器传了receipt_data等参数就一定要保存到数据库里。【下面会解释为什么】

    以上的校验步骤,可以有效的防止内购破解,下面内容是我看苹果官方能文档的关于in_app这个参数说明和解释下为啥服务器必须要保存每一个不同的receipt_data。

    苹果IAP官方文档

    苹果文档上介绍in_app参数内容截图

    In the JSON file, the value of this key is an array containing all in-app purchase receipts based on the in-app purchase transactions present in the input base-64 receipt-data. For receipts containing auto-renewable subscriptions, check the value of the latest_receipt_info key to get the status of the most recent renewal.

    大概意思是说:

    在这个JSON文件中,这个键的值是一个数组,该数组包含基于base-64后的所有内购收据。如果你的内购类型是自动更新订阅,那么请通过检查latest_receipt_info键的值,来确定最近更新的状态。

    很有意思的是,苹果还特别标明了这么一句话:

    Note: An empty array is a valid receipt.

    也就是说这个in_app参数可能为空,如果为空的话,也需要把这笔交易认为是有效的交易。这是苹果建议的操作。当然我们肯定不能这么干,这个参数是必须必须要校验的,不然越狱环境下,分分钟就把你内购破解了。我去校验了很多正常用户的内购订单,没发现一个in_app参数是为空的。但为了保险,还是让后台把所有前端传的receipt_data等参数不管成功失败都保存下来,万一哪个用户因此投诉充值不到账,我们有据可查。

    下面两段话

    The in-app purchase receipt for a consumable product is added to the receipt when the purchase is made. It is kept in the receipt until your app finishes that transaction. After that point, it is removed from the receipt the next time the receipt is updated - for example, when the user makes another purchase or if your app explicitly refreshes the receipt.

    The in-app purchase receipt for a non-consumable product, auto-renewable subscription, non-renewing subscription, or free subscription remains in the receipt indefinitely.

    大概意思是说:

    每当有一笔交易发起的时候,in_app里就会添加收据的一些信息。这些信息会一直保存直到你结束这笔交易。在此之后,下次更新收据时会将其从收据中删除 - 例如,当用户再次购买时,或者您的应用明确刷新收据时。

    非消耗型项目,自动续期订阅,非续期订阅或免费订阅的应用内购买收据将无限期保留在收据中。

    这一点也解释了说,为什么in_app这个数组有时候会有多个元素。

    以下内容和本篇文章无关,下面只是简单分享下我了解到的越狱,以及我身边关于越狱的一些事情和对这件事情的反思。

    关于越狱的一些事情:

    现在越狱已经支持iOS11.3了,我的安卓同事前阵子用自己的iPhone X 越狱了。装了下插件【付费的 并且需要美国区App Store账号】他是我们产品的重度用户,很多小号。但偏偏产品因为某些原因不让做退出登录,切换账号的功能。所以一般用户如果想切换账号,只能卸载重装。但他就不一样了,他骚的飞起,在越狱机上装的插件,他可以改我的所有代码。我看了一眼。整个项目的类,方法,返回值,入参,在那个插件下一览无遗。他找到了我保存用户信息的方法,因为是公司自己人,各种业务流程,他都懂。篡改了一些我的方法,返回参数。比如一些判断bool的方法,应该返回NO,直接篡改成YES。还一些其他的东西。当然后台本身对安全的逻辑校验控制的比较弱,只在一些关键接口【支付 送礼等】做了控制。

    抖音的国际板Tik Tok 如果你在国内切换成美区的App Store账号,并用vpn下载后,你会发现接口刷不到数据,看不到国外的小姐姐。我同事篡改了抖音的方法,就可以拉到数据了。具体方法。[PS:日本人发的抖音感觉都傻乎乎的,上面的小姐姐比国内的差远了。水平【化妆 滤镜 美颜 拍摄角度】明显不如国内的666]

    类名:CTCarrier 方法:- (id) isoCountryCode 该方法是系统方法 拿来获取电话服务商的iOS国家编码。改成日本就是拿日本数据,改成香港就是香港数据

    修改支付宝步数

    类名:APStepInfo 方法:- (long long) numberofSteps 返回值就是步数,你随便改成几万步都可以。

    修改支付宝朋友小红点

    类名:MPBadgeView 方法:- (void) drawBadgeRedPoint 

    另外,还有各种被玩坏的逆向微信功能。

    是的,就是这么简单。在越狱环境下。我们写的App犹如一个被剥光衣服的小姑娘,只要越狱+V【和】P【谐】N+一些付费插件,谁都可以过来上几下。我的安卓同事不会写任何iOS的代码,不懂汇编,不懂砸壳,不懂啥iOS逆向,但他懂只要我付费几美元买个插件,一个个App就是一个个脱光衣服的姑娘。是的,现在破解的门槛真的很低,你花点钱站在巨人的肩膀上就可以为所欲为。

    另外上面的几个例子,不知道大家发现了没。方法名,类名基本上都是见名知意,这也正是我们iOS的规范所在。严格的命名方式,让人一目了然,也让破解者一目了然。我大概看了,国内大厂App。抖音,支付宝,微信等等这些,没有一家做混淆的。方法名都挺规范的。这时候,我又想起了之前和同事开玩笑说的话,“以后再也特么不歧视命名-(void)a -(void)b -(void)c,label1 label2 label3的人了,人家自带混淆”。这里,我也很奇怪,虽然我知道其实就算你做了混淆对攻击者来说也是没卵用,但好歹之前是裸体小姑娘,现在是穿衣服的小姑娘了,总是会提高一些门槛把。细思了一下,大概是真的因为这玩意工作量大,容易引起很多潜在bug,收益又很低【攻击者无法是秒破变成天破而已,慢慢试总能试出来】,性价比极低,所以才不做的把。

    另外,可能也有朋友会说,既然越狱机下这么多搞事情的,我能不能写个方法直接禁掉越狱机子啊。反正越狱用户也不会多,就不要了。答案是否定的,网上的一些代码,你去搜iOS判断是否越狱,iOS越狱检测等等。出来的文章基本都是复制粘贴,没什么价值的,里面的代码古老且旧,尤其是检测越狱时方法用bool值返回的那种,在一些防越狱检测的插件下,更是被秒破的。有价值的资料比我想象中的要更少。如果有朋友能有一些最新防越狱插件检测的方法,请不吝分享下。

    下面在举几个大家做内购经常遇到的一些问题,和一些容易混淆的点。

    Q1:内购和Apple Pay的区别?

    A1:内购是内购,Apple Pay是Apple Pay。我不知道有多少人第一次接触时,会把这俩概念混淆掉,这里你可以简单这么理解,虚拟的物品就是用内购,实际的物品就是用Apple Pay。Apple Pay是一种支付方式,你可以类比为支付宝,微信那种。但人家只支持实际物品,如果你东西是虚拟的话,你却集成Apple Pay上架是要被拒绝的哦~当然反过来,实际物品你却集成内购上架,也是一样被拒。对于大部分的国内开发者而言,你很少会遇到需要集成Apple Pay的App的。能用支付宝/微信的场景还要求支持Apple Pay的产品毕竟是少数。

    Q2:内购项目的类型区别?

    A2:首先内购项目分为以下4种,消耗型项目,非消耗型项目,自动续期订阅,非续期订阅。我们来一个个介绍。

    消耗型项目:只可使用一次的产品,使用之后即失效,必须再次购买。就是大家最广为所知的虚拟币,比如直播平台斗鱼的鱼翅,熊猫的竹子,哔哩哔哩的B币等,这个概念大家应该很好理解,不过多解释了。

    非消耗型项目:只需购买一次,不会过期或随着使用而减少的产品。这个一般是游戏那里用的多,一般是付费解锁关卡的场景,用户买过一次,卸载重装或者同一个Apple id但换App账号时,也要能保证用户重新获得该内购商品。所以App内部需要额外去实现恢复购买的逻辑。

    自动续期订阅:允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期。iTunces上给的示例是:每月订阅提供流媒体服务的 App。对比我们熟悉的,网易云音乐的内购商品-连续包月黑胶VIP,就是此类型。一般来说,没啥必要不要选这一种,如果是VIP的那种场景推荐下面非续期订阅类型去做。自动续期订阅的坑非常多,比另外几种内购类型都要复制。

    非续期订阅:一般来说VIP可以用这种方法来做订阅,我们公司项目的VIP购买就是这种方式。他的实现方式你可以完全照搬消耗性项目,不用做什么额外处理,也不用去管返回的订阅日期什么的东西,就是以服务器那边为准。服务器的日期开始,服务器的日期结束。既简单又保险,不需要额外的做什么处理。

    Q3:VIP一定要用内购做吗?

    A3:其实判断你们公司的App到底需不需要用内购,很简单,就是看跟实际物品有没有关系。如果你的VIP功能是类似饿了么这种,点外卖可以打折/多领红包 那么就不需要用内购,上架的时候说清楚就行了。如果你的VIP功能是虚拟的,比如头像更炫酷,尊贵的VIP身份标示,独特的入场动画等等虚拟相关的,比如QQ会员,就必须要用内购去做。需要说明的是,那种是VIP才能和某某用户聊天的场景,是VIP才能得到App里某某用户的服务【语音,视频】时,这一类的场景苹果一样认为是虚拟的,一样要用内购去做。

    Q4:VIP内购一定是非续期/自动续期订阅吗?我可不可以用虚拟币购买VIP呢?

    A4:这个问题我自己经历过。我的答案是你也可以用虚拟币购买VIP的这种方式,但如果被拒绝,你只能老老实实的按前种方式去做。如果你们的App既有虚拟币又有VIP,产品希望你VIP是直接用虚拟币去购买,这样整个流程都很方便。那么你一定要记住。千万不要在1.0版本这么做,这是血泪教训。1.0版本会抓的很严很严,同时虚拟币+VIP功能,百分百苹果会要求你VIP要用续期订阅去实现。最保险的做法呢,1.0版本不要做任何内购,迭代几个小版本后,加入虚拟币内购,在迭代几个小版本,加入VIP直接用虚拟币购买的功能,这是最最保险的做法。记住:1.0的审核力度是真的很严,能先不做内购就不要做内购,老板或许不懂,1.0版本什么都想要,但往往因为内购,会让你们的产品反复被拒。这一块如果大家感兴趣,可以看看,我的1.0版本就是加了内购,反复被拒5次。血泪教训

    Q5:我们老板心疼那百分之30的手续费,我能不能不用内购啊,有没办法绕过内购?

    A5:办法是有的。但是有风险。我16年做过绕开内购的方法。思路很简单,就是App里集成支付宝/微信/内购这种功能,后台做控制开关,审核时,开关打开,给审核人员看内购功能,审核通过后,开关关闭,给正常用户是用支付宝/微信功能。这个方法,我17年的时候听到很多群友说不行了,你在上架审核时,苹果会扫描你的包,检测到第三方支付sdk时,会拒绝掉。后来又有群友说可以用H5的方式实现支付功能。另外可能会有别的绕开苹果审核的实现方式,如果有哪位朋友知道,不妨留言告知。但不管是哪种方式,都是有风险的,苹果对内购一直抓的很严,如果让它知道你们在钱的方面上欺骗过他,后果还是很严重的。iOS上的用户付费率还是很不错的,付费意愿基本上可以是安卓用户的十几倍。所以如果你们的产品真的有前景,并且想长久做下去,还是奉劝不要做欺骗苹果的事情了。

    Q6:网上有好多讲丢单的博客,看的是一脸懵逼,有的看懂后,在看下一篇又不懂了,感觉都好复杂。

    A6:我在刚接触内购的时候也是这样,我觉得有些博客讲的真有点过了,它为了考虑一些用户的极端操作,多出来很多逻辑处理,导致博客异常的复杂,我记得有博客讲必须要把receipt_data等信息存到keychain里,因为用户有可能卸载App,如果你只存到NSUserDefaults里,那样就丢单了。 ......那么有没有这种情况呢?我觉得是肯定有的。但我们来算算几率,首先他内购成功,在向服务器调接口的时候,他手机突然没电了/断网了/程序崩溃了/网络差等的久他自己杀死进程了 巴拉巴拉。 然后他在下一次手机恢复正常的时候,果断卸载掉App,重新去App Store上下载安装,进入App后,发现内购没到账。

    网上博客还爱用那种切换账号的场景举例,A内购成功了,但用户各种骚操作后,自己换到B账号,然后服务器那边把商品发到B账号上了,等等。

    这些情况都是存在的,因为苹果的内购机制问题,你是不能百分百保证不丢单的,不要把丢单情况看的那么严重,逻辑写的那么复杂。你看看所有大厂的App上都会写充值遇到问题,点我联系客服 巴拉巴拉。关于丢单,我的做法是这样的,在苹果内购成功的回调里,NSUserDefaults存每一笔支付成功的订单,如果服务器校验成功,就把本地存的这笔订单删除。如果没收到服务器的响应,就一直保留。然后每次App启动就会去把本地存的丢单信息扔向服务器校验,校验成功删除,校验失败不管。这里还是看开发时间,当时我写内购功能的时候,预算时间就两天不到,所以写的飞快,就简单的用这个办法去防止丢单,目前来看,没有发现过一笔真正用户充钱但商品没到账的例子。如果大家开发时间充足,可以慢慢去弥补极端操作漏洞。

    Q7:内购为什么会有这么多坑啊?看网上好多博客都在说,我自己做微信/支付宝的时候,没感觉有这么多坑啊

    A7:苹果的内购坑主要有以下几点

    applicationUsername该字段可能为nil 导致客户端没办法用这个参数给服务器透传订单编号,来形成一个交易订单号的绑定。

    校验订单流程是必须服务器主动去询问苹果服务器,而支付宝/微信 却是他们的服务器会在用户支付成功时主动给我们服务器回调。正是这个原因,让iOS开发者饱受折磨,大部分的丢单漏单都是苹果的这个设计造成的。苹果不会主动回调给我们服务器,也就意味着我们服务器需要主动去苹果那里询问这笔订单,到底成没成功。但服务器询问的时机,又是客户端告诉服务器的。这就鸡儿坑了,一些情况下,用户在付费成功后,突然断网了/崩溃了/出现意外了等等,客户端没办法告诉服务器,这就出现了,用户钱成功了,内购商品却没到账。所以网上才会有这么多篇讲防止丢单的博客。

    越狱下,插件也能破解掉苹果内购,然后校验状态status还返回成功。也就是本篇博客开头讲的那种情况。这一点真的是无力吐槽,亏你特么回调给我的receipt_data那么一大长串,有卵用?

    苹果的订单机制。苹果为了保护用户隐私,你是看不到一条条流水明细的。你看到的只有

    这种。

    每一种内购类型的总收入,或者总销量。导致对账查询的时候加了不少麻烦。

    苹果的退款机制。这个比上面一点更坑,iOS用户,内购了某商品,你可以在完全用完了后,联系苹果客服,说我误操作了巴拉巴拉或者说感觉这个商品不值那么多被开发者欺骗了巴拉巴拉,快给我退款,客服就会温柔的告诉你,不要急,她会帮你处理,1-2个工作日把,你就会发现你的钱就退回来了。没记错的话,一段时间内,一个Apple Id可以申请1-2次。但不能多,多了的话就会被苹果拒绝。而这一切,开发者这边是完全不知情的。你不知道哪个用户退款了,你知道的只是一个图,类似下面的这种。

    用户消费了你的内购商品,公司却收不到钱,很多公司的内购服务都是要成本的。如果这种用户一旦多起来,坏账率会飙升,公司就会被活活的拖垮。一个好的项目也就凉凉掉。淘宝上关于iOS内购退款专门有一个超级庞大的黑色产业链。从弄账号到专门联系苹果客服再到道具销赃变现,各司其职,一环套一环,每个环节人都赚的盆满钵满。苦的都是公司,因为苹果没有任何损失,他也不会补偿你公司1毛钱,一切损失都是公司自己承担。没记错的话,15-16年,很多很多游戏公司都是因为这个被活活拖垮的。幸运的是,这种恶意退款一般都是针对游戏公司,因为游戏道具可以快速变现。像正常的App甚少碰到,因为他退款了也没毛用,没法及时变现。毕竟他们可不稀罕跟你们的女用户1v1视频聊天。

    作者:羽化归来

    链接:https://www.jianshu.com/p/5cf686e92924

     

    展开全文
  • iOS 内购 大致步骤

    2021-02-28 18:53:17
    需要创建内购项目 核心代码(伪代码)导入头文件#import//开始充值请求 rechargeid是自己设置产品id-(void)startRechargeWithRechargeId:(NSString *)rechargeId {SKProductsRequest *queryRequest = [...

    你的苹果开发者账号需要完善银行税务等资料。不完成后面有大坑!

    fbddc45e6c1b814892ed18e1c39b4700.png

    需要创建内购项目

    36c28bb61a9fa2dd95ff02f40a3775f0.png

    核心代码(伪代码)

    导入头文件

    #import

    //开始充值请求 rechargeid是自己设置产品id

    -(void)startRechargeWithRechargeId:(NSString *)rechargeId {

    SKProductsRequest *queryRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:@[rechargeId]]];

    queryRequest.delegate = self;

    [queryRequest start];

    }```

    //充值查询回调

    ```-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {

    NSArray * recharges = response.products;

    if(recharges.count == 0){

    // 充值查询失败

    return;

    }

    SKProduct *sp = recharges[0];

    SKPayment *payment = [SKPayment paymentWithProduct:sp];

    [[SKPaymentQueue defaultQueue] addPayment:payment];

    }

    #pragma mark - 充值查询请求不成功

    -(void)request:(SKRequest *)request didFailWithError:(NSError *)error {

    if(error) {

    NSLog(@"失败原因>>>%@",error);

    [self dismissHUD];

    }

    }

    //反馈信息

    - (void)requestDidFinish:(SKRequest *)request {

    NSLog(@"充值信息>>>%@",request);

    }

    //完成交易事务

    -(void)completeTransaction:(SKPaymentTransaction *)transaction {

    //验证购买凭证

    [self verifyPruchaseWithRechargeId:transaction.payment.productIdentifier];

    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];

    }

    找后台验证凭据,凭据格式要和后台协商好(换个后台协商凭据格式就要搞一天)

    -(void)verifyPruchaseWithRechargeId:(NSString *)rechargeId {

    if(!rechargeId || rechargeId.length == 0){

    //验证充值失败!(获取充值ID失败,请联系客服)

    return;

    }

    //获取验证凭据

    NSData *receiptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];

    if(!receiptData || receiptData.length == 0){

    [self showImplyInfo:@"验证充值失败!(获取充值凭证失败,请联系客服)" isActive:NO];

    return;

    }

    // 下面凭证转化取一种就行,看后台习惯哪种

    // 凭证可以转为base64

    receiptHex = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];

    //或者凭证转换为hex

    receiptHex = [self hexFromData:receiptData];

    }

    转化方法

    -(NSString *)hexFromData:(NSData *)data {

    if(!data) return nil;

    NSMutableString *hex = [NSMutableString string];

    char *chars = (char *)data.bytes;

    for(NSUInteger i = 0; i < data.length; i++) {

    [hex appendFormat:@"%0.2hhx",chars[i]];

    }

    return hex;

    }

    展开全文
  • iOS 内购最新讲解

    2020-12-19 11:15:45
    总说内购的内容协议、税务和银行业务 信息填写内购商品的添加添加沙盒测试账号内购代码的具体实现内购的注意事项二.协议、税务和银行业务 信息填写2.1、协议、税务和银行业务 信息填写 的入口协议、税务和银行业务 ...
  • iOS内购三:Receipt

    2020-09-10 11:55:20
    iOS内购三:Receipt 可参考: Validating Receipts Locally 需验证receipt,可以在本地验证,也可以在服务端验证 本地验证,涉及到security和加密,比较复杂 而服务端验证相对而言简单些 收到transaction后,将...
  • iOS 内购详解-代码篇

    千次阅读 2020-07-04 16:29:34
    内购项目-代码篇一、分步骤说明1、获取商品列表2、苹果服务器返回的可购买商品3、下单购买商品4、购买队列状态变化,判断购买状态是否成功5、交易验证6、拿到的收据信息是,此App所有购买的记录7、恢复商品二、详细...
  • iOS内购--java后台

    千次阅读 2019-02-15 09:49:15
    最近公司iOS发布了新版本,被拒,原因就是没有添加内购,并被严重警告,为此,不得已要加上iOS内购功能,以下就是我为了iOS内购所写的后台代码,首先看下支付的时序图吧: 简单说下,时序图的意思吧: 第一...
  • iOS原生内购代码, 支持消耗型内购,一次性内购和订阅, 原生ObjectC编写封装,支持最新iOS 13系统!
  • Unity使用IAP接入IOS内购工程,接入过程和介绍可查看博文:https://czhenya.blog.csdn.net/article/details/120173348
  • iOS 内购遇到的坑

    2020-12-21 01:19:26
    您以购买过此APP内购项目,此项目将免费恢复您以购买过此APP内购项目,此项目将免费恢复.PNG原因:当使用内购购买过商品后没有把这个交易事件关,所以当我们再次去购买商品后就会调用以前已经购买成功的交易事件去...
  • 一、内购项目的创建 游戏内购项目就跟商品一样,需要在appstoreconnect后台对应用下创建APP内购买项目 app内购项目分为四种 消耗型项目–这类内购项目是指游戏内一次性购买的道具,元宝等 非消耗型项目 – 指购买后...
  • ios 内购 恢复内购

    千次阅读 2019-08-28 15:33:57
    申请内购部分略。。。。。。。 代码部分 1 //准备工作 [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; <SKPaymentTransactionObserver,SKProductsRequestDelegate> //遵循代理 打开支付开关 ...
  • <p>ios内购怎么从获取内购的商品信息,有在后台服务端获取这种方式吗?求大神解答</p>
  • iOS内购测试自动订阅型产品

    千次阅读 2020-09-09 17:48:16
    最近产品需要涉及到订阅型的内购创建,上一次使用自动订阅内购还是好几年的事情了,很多内容大都忘得差不多了,加上苹果设计的内购又增加了一些新功能和改变,不得不重新来审视下自动订阅型内购,而官方又没有中文...
  • iOS-iOS内购流程(手把手图文教程)

    千次阅读 2020-01-05 21:54:55
    上家公司做了一个APP,可以充值虚拟金币,但是如果是虚拟道具,就必须使用苹果内购,不然审核过不了,而且很黑,三七分;当然,你如果购买真是东西,比如淘宝、京东等等就不需要了!这里我就来说说苹果内购流程,...
  • Objective-C iOS StoreKit 原生内购订阅代码封装,语言是Objective-C,内含demo,可以二次封装给其他开发工具使用. 支持Unity3D,cocos,xamarin...等开发工具进行二次封装使用.
  • 苹果审核支付功能哪些属于内购哪些不属于内购? 1.首先,我们要知道哪些是产品需要内购,哪些不需要内购; 2.走不走内购,主要看我们付费的产品,是留存在App内使用和操作;还是在App外部使用;比如说:我们付费的...
  • 内购返回商品列表ID为空 之前也遇到过,都是商品Id传错了导致的,但是这次再次遇到让我又学习到了。记录一下找问题的过程: 还是以为是商品Id的问题,比对之后发现是一样的,在自己的demo上是可以请求到商品的,在...
  • Unity接入iOS内购

    2020-07-13 17:39:27
    1.内购种类 consumable:可消费的,如游戏中的金币,用完还可以再购买。 non-consumable:不可销毁的,一次购买,永久生效。比如去广告,解锁游戏关卡,这种商品只能购买一次。 subscription:订阅的,这种一般用于...
  • Unity iOS内购

    千次阅读 2016-10-31 23:00:31
    Unity iOS内购思路:Unity调用iOS内购代码实现效果图: 流程这里就不重复写了,直接上截图 OC代码:IAPInterface(主要是实现Unity跟OC的IAP代码的一个交互作用,等于是一个中间桥梁)#import <Foundation

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 26,606
精华内容 10,642
关键字:

ios内购

友情链接: IV_PROFINET_TIA PORTAL.zip