21002 iap ios_iap ios - CSDN
  • 一. 验证 1.完成购买后读取本地的 receiptData 进行 base64 编码,转换成字符串 //读取 NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL]; NSData *receiptData = [NSData dataWithContentsOfURL:...

    一. 验证

    1.完成购买后读取本地的 receiptData 进行 base64 编码,转换成字符串

    //读取
    NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
    NSString *receiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    复制代码

    2.发送接口到苹果服务器进行验证

    • 沙盒验证 https://sandbox.itunes.apple.com/verifyReceipt
    • 正式环境 https://buy.itunes.apple.com/verifyReceipt

    请求体为 JSON字符串 {"receipt-data":receiptString} (receiptString 就是第一步拿到的字符串)

    此处建议优先验证线上服务器,如果出现 21007 错误码时,表明这是个沙盒环境的数据,然后进行沙盒环境的验证即可。下面是可能出现的错误码

    错误码

    • 21000 App Store无法读取你提供的JSON数据
    • 21002 收据数据不符合格式
    • 21003 收据无法被验证
    • 21004 你提供的共享密钥和账户的共享密钥不一致
    • 21005 收据服务器当前不可用
    • 21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
    • 21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证
    • 21008 收据信息是产品环境中使用,但却被发送到测试环境中验证

    如果进行服务器验证的话,将拿到的 receiptString 发送给服务器,由服务器进行下一步的验证。

    楼主此处吃了一次亏。后端是PHP,百度上能搜出来的验证代码,其实是有问题的,主要在于后台拿到 receiptString 后,进行 JSON 字符串的生成,如果PHP 直接使用 json_encode(**) 的话,会导致 receiptString 中出现的 \ 进行进一步的处理,变成 \\,从而导致验证出现 21002 的错误。解决方案也很简单,直接使用字符串拼接一个 JSON 出来,然后请求苹果服务器即可

    展开全文
  • 1. 准备工作,先到iTunesConnect 上进行添加内购项目和沙盒... 创建APP 内购项目 选择其中一种类型,进行创建 ...填写内购项目的内容(其中的产品 ID 记住,在接下来有很多地方需要用到) ...在创建测试员的时候,记住...

    1. 准备工作,先到iTunesConnect 上进行添加内购项目和沙盒测试账号

    创建APP 内购项目

    在这里插入图片描述

    选择其中一种类型,进行创建

    在这里插入图片描述

    填写内购项目的内容(其中的产品 ID 记住,在接下来有很多地方需要用到)

    在这里插入图片描述

    屏幕快照一定要选到,不然会显示该内购项目元数据丢失

    在这里插入图片描述

    状态显示为黄色:准备提交才是正确的

    在这里插入图片描述

    2. 创建沙盒账号

    在这里插入图片描述
    在这里插入图片描述
    在创建测试员的时候,记住那个邮箱不能填你已经有的 AppleID 的邮箱,填好后会有一篇验证信息发到你刚刚填写的邮箱,验证后就可以用了

    3. unity 里面的操作

    将 IAP 相关的文件导入 unity 中

    在这里插入图片描述

    点击 in-App-Purchasing进去

    在这里插入图片描述
    保证 Analytics 和in-App-Purchasing是打开状态就可以了

    添加 IAPButton

    在这里插入图片描述

    填写 IAPButton 里面的信息

    在这里插入图片描述
    在这里插入图片描述

    • 新建一个脚本
    
    
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Purchasing;
    using UnityEngine.UI;
    using System.Text;
    using System;
    using MiniJSON;
    using UnityEngine.Purchasing.Security;
    
    
    public class PurchaseManager : MonoBehaviour, IStoreListener
    {
    	
    	
    	private IStoreController controller;
    	ConfigurationBuilder builder;
    
    	private IStoreController m_Controller;
    	private IAppleExtensions m_AppleExtensions;
    
    	void Start()
    	{
    		var module = StandardPurchasingModule.Instance();
    		builder = ConfigurationBuilder.Instance(module);
    		builder.AddProduct("产品 ID", ProductType.Subscription);
    		UnityPurchasing.Initialize(this, builder);
    
    
    	}
    
    	/// <summary>
    	/// Called when Unity IAP is ready to make purchases.
    	/// </summary>
    	public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    	{
    		this.controller = controller;
    		m_Controller = controller;
    		m_AppleExtensions = extensions.GetExtension<IAppleExtensions> ();
    		m_AppleExtensions.RegisterPurchaseDeferredListener (OnDeferred);
    	}
    
    	/// <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)
    	{
    	}
    
    	/// <summary>
    	/// Called when a purchase completes.
    	///
    	/// May be called at any time after OnInitialized().
    	/// </summary>
    	//这个函数就是购买成功后自动调用的函数
    	public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
    	{
    		Debug.Log ("IAP成功");
    		if (Application.platform == RuntimePlatform.IPhonePlayer ||
    			Application.platform == RuntimePlatform.tvOS) {
    			string transactionReceipt = m_AppleExtensions.GetTransactionReceiptForProduct (e.purchasedProduct);
    			Debug.Log (transactionReceipt);
    			//这里的transactionReceipt是 Base64加密后的交易收据,将这个收据发送给远程服务器就会得到订单信息的 Json 数据
    
    		}    
    		return PurchaseProcessingResult.Complete;
    	}
    
    	/// <summary>
    	/// Called when a purchase fails.
    	/// </summary>
    	public void OnPurchaseFailed(Product item, PurchaseFailureReason r)
    	{
    		Debug.Log ("IAP失败");
    		Debug.Log ("失败原因:"+r.ToString());
    	}
    
    
    //将这个函数挂载到需要进行内购的 UI(Button)上
    	public void OnPurchaseClicked(string productId)
    	{
    		controller.InitiatePurchase(productId);
    	}
    
    		
    	private void OnDeferred (Product item)
    	{
    		Debug.Log ("Purchase deferred: " + item.definition.id);
    	}
    }
    	
    
    

    在要进行内购的 Button 上进行挂载方法

    在这里插入图片描述

    4. 打包到 Xcode 里面进行沙盒测试

    在运行之前记得把 IAP 打开,还有将真机的 AppleID注销再进行测试

    在这里插入图片描述

    5. 验证订单信息

    在购买成功后自动调用的函数里面

    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
    	{
    		Debug.Log ("IAP成功");
    		if (Application.platform == RuntimePlatform.IPhonePlayer ||
    			Application.platform == RuntimePlatform.tvOS) {
    			string transactionReceipt = m_AppleExtensions.GetTransactionReceiptForProduct (e.purchasedProduct);
    			Debug.Log (transactionReceipt);
    			//这里的transactionReceipt是 Base64加密后的交易收据,将这个收据发送给远程服务器就会得到订单信息的 Json 数据
    
    		}    
    		return PurchaseProcessingResult.Complete;
    	}
    

    使用transactionReceipt发送请求
    在沙盒环境中,发送transactionReceipt苹果的测试服务器( https://sandbox.itunes.apple.com/verifyReceipt )验证

    在正式服务器中(已上线Appstore),发送transactionReceipt到苹果的正式服务器( https://buy.itunes.apple.com/verifyReceipt )验证
    验证后返回的Json 数据如下:

    {
        "receipt": {
            "original_purchase_date_pst": "2017-11-15 15:25:20 America/Los_Angeles",
            "purchase_date_ms": "1510788320209",
            "unique_identifier": "0ea7808637555b2c633eb07aa1cb0894c821a6f9",
            "original_transaction_id": "1000000352597239",
            "bvrs": "0",
            "transaction_id": "1000000352597239",
            "quantity": "1",
            "unique_vendor_identifier": "01B57C2E-9E91-42FF-9B0D-4983175D6694",
            "item_id": "1141751870",
            "original_purchase_date": "2017-11-15 23:25:20 Etc/GMT",
            "product_id": "100.gold.coins",
            "purchase_date": "2017-11-15 23:25:20 Etc/GMT",
            "is_trial_period": "false",
            "purchase_date_pst": "2017-11-15 15:25:20 America/Los_Angeles",
            "bid": "com.unity3d.unityiap.demo",
            "original_purchase_date_ms": "1510788320209"
        },
        "status": 0
    }
    

    参考文章:
    https://www.jianshu.com/p/4045ebf81a1c
    https://docs.unity3d.com/Manual/UnityIAPiOSMAS.html

    展开全文
  • 我在前端支付后,将receipt传到java服务端进行验证,并将receipt进行...但是始终收到结果:{"status":21002, "exception":"java.lang.IllegalArgumentException"} 请问各位大神有没有遇到过一样问题的,求大神帮忙解决
  • iOS开发中如果涉及到虚拟物品的购买,就需要使用IAP服务,我们今天来看看如何实现。 在实现代码之前我们先做一些准备工作,一步步来看。 1、IAP流程 IAP流程分为两种,一种是直接使用Apple的服务器进行购买和...

    在iOS开发中如果涉及到虚拟物品的购买,就需要使用IAP服务,我们今天来看看如何实现。

    在实现代码之前我们先做一些准备工作,一步步来看。

    1、IAP流程

    IAP流程分为两种,一种是直接使用Apple的服务器进行购买和验证,另一种就是自己假设服务器进行验证。由于国内网络连接Apple服务器验证非常慢,而且也为了防止黑客伪造购买凭证,通用做法是自己架设服务器进行验证。

    下面我们通过图来看看两种方式的差别:

    1.1、使用Apple服务器

    这里写图片描述

    1.2、自己架设服务器

    这里写图片描述

    简单说下第二中情况的流程:

    1.用户进入购买虚拟物品页面,App从后台服务器获取产品列表然后显示给用户
    
    2.用户点击购买购买某一个虚拟物品,APP就发送该虚拟物品的productionIdentifier到Apple服务器
    
    3.Apple服务器根据APP发送过来的productionIdentifier返回相应的物品的信息(描述,价格等)
    
    4.用户点击确认键购买该物品,购买请求发送到Apple服务器
    
    5.Apple服务器完成购买后,返回用户一个完成购买的凭证
    
    6.APP发送这个凭证到后台服务器验证
    
    7.后台服务器把这个凭证发送到Apple验证,Apple返回一个字段给后台服务器表明该凭证是否有效
    
    8.后台服务器把验证结果在发送到APP,APP根据验证结果做相应的处理
    

    2、iTunes Connet操作

    搞清楚了自己架设服务器是如何完成IAP购买的流程了之后,我们下一步就是登录到iTunes Connet创建应用和指定虚拟物品价格表

    2.1、创建自己的App

    如下图所示,我们需要创建一个自己的APP,要注意的是这里的Bundle ID一定要跟你的项目中的info.plist中的Bundle ID保证一致。也就是图中红框部分。

    这里写图片描述

    2.2、创建虚拟物品价格表

    2.2.1、虚拟物品分为如下几种:

    1.消耗品(Consumable products):比如游戏内金币等。
    
    2.不可消耗品(Non-consumable products):简单来说就是一次购买,终身可用(用户可随时从App Store restore)。
    
    3.自动更新订阅品(Auto-renewable subscriptions):和不可消耗品的不同点是有失效时间。比如一整年的付费周刊。在这种模式下,开发者定期投递内容,用户在订阅期内随时可以访问这些内容。订阅快要过期时,系统将自动更新订阅(如果用户同意)。
    
    4.非自动更新订阅品(Non-renewable subscriptions):一般使用场景是从用户从IAP购买后,购买信息存放在自己的开发者服务器上。失效日期/可用是由开发者服务器自行控制的,而非由App Store控制,这一点与自动更新订阅品有差异。
    
    5.免费订阅品(Free subscriptions):在Newsstand中放置免费订阅的一种方式。免费订阅永不过期。只能用于Newsstand-enabled apps。
    

    类型2、3、5都是以Apple ID为粒度的。比如小张有三个iPad,有一个Apple ID购买了不可消耗品,则三个iPad上都可以使用。

    类型1、4一般来说则是现买现用。如果开发者自己想做更多控制,一般选4

    2.2.2、创建成功后如下所示:

    这里写图片描述

    其中产品id是字母或者数字,或者两者的组合,用于唯一表示该虚拟物品,app也是通过请求产品id来从apple服务器获取虚拟物品信息的。

    2.3、设置税务和银行卡信息

    这一步必须设置,不然是无法从apple获取虚拟产品信息。

    设置成功后如下所示:

    这里写图片描述


    3、iOS端具体代码实现

    完成了上面的准备工作,我们就可以开始着手IAP的代码实现了。

    我们假设你已经完成了从后台服务器获取虚拟物品列表这一步操作了,这一步后台服务器还会返回每个虚拟物品所对应的productionIdentifier,假设你也获取到了,并保存在属性self.productIdent中。

    需要在工程中引入 storekit.framework

    我们来看看后续如何实现IAP

    3.1、确认用户是否允许IAP

    //移除监听
    -(void)dealloc
    {
        [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
    }
    
    //添加监听
    - (void)viewDidLoad{
        [super viewDidLoad];
        [self.tableView.mj_header beginRefreshing];
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
    
    - (void)buyProdution:(UIButton *)sender{    
        if ([SKPaymentQueue canMakePayments]) {
            [self getProductInfo:self.productIdent];
        } else {
            [self showMessage:@"用户禁止应用内付费购买"];
        }
    }

    3.2、发起购买操作

    如果用户允许IAP,那么就可以发起购买操作了

    //从Apple查询用户点击购买的产品的信息
    - (void)getProductInfo:(NSString *)productIdentifier {
        NSArray *product = [[NSArray alloc] initWithObjects:productIdentifier, nil];
        NSSet *set = [NSSet setWithArray:product];
        SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
        request.delegate = self;
        [request start];
        [self showMessageManualHide:@"正在购买,请稍后"];
    }
    
    // 查询成功后的回调
    - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
        [self hideHUD];
        NSArray *myProduct = response.products;
        if (myProduct.count == 0) {
            [self showMessage:@"无法获取产品信息,请重试"];
            return;
        }
        SKPayment * payment = [SKPayment paymentWithProduct:myProduct[0]];
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
    
    //查询失败后的回调
    - (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
        [self hideHUD];
        [self showMessage:[error localizedDescription]];
    }

    3.3、购买操作后的回调

    //购买操作后的回调
    - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
        [self hideHUD];
        for (SKPaymentTransaction *transaction in transactions)
        {
            switch (transaction.transactionState)
            {
                case SKPaymentTransactionStatePurchased://交易完成
                    self.receipt = [GTMBase64 stringByEncodingData:[NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]]];
                    [self checkReceiptIsValid];//把self.receipt发送到服务器验证是否有效
                    [self completeTransaction:transaction];
                    break;
    
                case SKPaymentTransactionStateFailed://交易失败
                    [self failedTransaction:transaction];
                    break;
    
                case SKPaymentTransactionStateRestored://已经购买过该商品
                    [self showMessage:@"恢复购买成功"];
                    [self restoreTransaction:transaction];
                    break;
    
                case SKPaymentTransactionStatePurchasing://商品添加进列表
                    [self showMessage:@"正在请求付费信息,请稍后"];
                    break;
    
                default:
                    break;
            }
        }
    
    }
    
    
    
    - (void)completeTransaction:(SKPaymentTransaction *)transaction {
        [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
    }
    
    
    - (void)failedTransaction:(SKPaymentTransaction *)transaction {
        if(transaction.error.code != SKErrorPaymentCancelled) {
            UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:@"购买失败,请重试"delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"重试", nil];
            [alertView show];
        } else {
            [self showMessage:@"用户取消交易"];
        }
    
        [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
    }
    
    
    - (void)restoreTransaction:(SKPaymentTransaction *)transaction {
        [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
    }

    3.4、向服务器端验证购买凭证的有效性

    在这一步我们需要向服务器验证Apple服务器返回的购买凭证的有效性,然后把验证结果通知用户

    - (void)checkReceiptIsValid{
    
        AFHTTPSessionManager manager]GET:@"后台服务器地址"  parameters::@"发送的参数(必须包括购买凭证)"
        success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {
            if(凭证有效){
              你要做的事
            }else{//凭证无效
              你要做的事
            }
    
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:@"购买失败,请重试"delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"重试", nil];
                [alertView show];
        }
    
    }

    3.5、发送凭证失败的处理

    如果出现网络问题,导致无法验证。我们需要持久化保存购买凭证,在用户下次启动APP的时候在后台向服务器再一次发起验证,直到成功然后移除该凭证。
    保证如下define可在全局访问:

    #define AppStoreInfoLocalFilePath [NSString stringWithFormat:@"%@/%@/", [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject],@"EACEF35FE363A75A"]
    
    -(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
    {
        if (buttonIndex == 0)
        {
            [self saveReceipt];
        }
        else
        {
            [self checkReceiptIsValid];
        }
    }
    
    //AppUtils 类的方法,每次调用该方法都生成一个新的UUID
    + (NSString *)getUUIDString
    {
        CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault);
        CFStringRef strRef = CFUUIDCreateString(kCFAllocatorDefault , uuidRef);
        NSString *uuidString = [(__bridge NSString*)strRef stringByReplacingOccurrencesOfString:@"-" withString:@""];
        CFRelease(strRef);
        CFRelease(uuidRef);
        return uuidString;
    }
    
    //持久化存储用户购买凭证(这里最好还要存储当前日期,用户id等信息,用于区分不同的凭证)
    -(void)saveReceipt{
        NSString *fileName = [AppUtils getUUIDString];
        NSString *savedPath = [NSString stringWithFormat:@"%@%@.plist", AppStoreInfoLocalFilePath, fileName];
    
        NSDictionary *dic =[ NSDictionary dictionaryWithObjectsAndKeys:
                            self.receipt,                           Request_transactionReceipt,
                            self.date                               DATE                        
                            self.userId                             USERID
                            nil];
    
        [dic writeToFile:savedPath atomically:YES];
    }

    3.6、APP启动后再次发送持久化存储的购买凭证到后台服务器

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{    
        NSFileManager *fileManager = [NSFileManager defaultManager];
    
        //从服务器验证receipt失败之后,在程序再次启动的时候,使用保存的receipt再次到服务器验证
        if (![fileManager fileExistsAtPath:AppStoreInfoLocalFilePath]) {//如果在改路下不存在文件,说明就没有保存验证失败后的购买凭证,也就是说发送凭证成功。
            [fileManager createDirectoryAtPath:AppStoreInfoLocalFilePath//创建目录
                   withIntermediateDirectories:YES
                                    attributes:nil
                                         error:nil];
        }
        else//存在购买凭证,说明发送凭证失败,再次发起验证
        {
            [self sendFailedIapFiles];
        }
    }
    
    //验证receipt失败,App启动后再次验证
    - (void)sendFailedIapFiles{
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSError *error = nil;
    
        //搜索该目录下的所有文件和目录
        NSArray *cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:AppStoreInfoLocalFilePath error:&error];
    
        if (error == nil)
        {
            for (NSString *name in cacheFileNameArray)
            {
                if ([name hasSuffix:@".plist"])//如果有plist后缀的文件,说明就是存储的购买凭证
                {
                    NSString *filePath = [NSString stringWithFormat:@"%@/%@", AppStoreInfoLocalFilePath, name];
                    [self sendAppStoreRequestBuyPlist:filePath];
    
                }
            }
        }
        else
        {
            DebugLog(@"AppStoreInfoLocalFilePath error:%@", [error domain]);
        }
    }
    
    -(void)sendAppStoreRequestBuyPlist:(NSString *)plistPath
    {
        NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:plistPath];
    
        //这里的参数请根据自己公司后台服务器接口定制,但是必须发送的是持久化保存购买凭证
        NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                  [dic objectForKey:USERID],                           USERID,                    
                  [dic objectForKey:DATE],                             DATE,  
                  [dic objectForKey:Receipt],                            Receipt,                                                                             
                  nil];
    
    
            AFHTTPSessionManager manager]GET:@"后台服务器地址"  parameters:params  success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {
            if(凭证有效){
             [self removeReceipt]
            }else{//凭证无效
              你要做的事
            }
    
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
    
        }
    
     }
    
    //验证成功就从plist中移除凭证
    -(void)sendAppStoreRequestSucceededWithData
    {
        NSFileManager *fileManager = [NSFileManager defaultManager];
        if ([fileManager fileExistsAtPath:AppStoreInfoLocalFilePath])
        {
            [fileManager removeItemAtPath:AppStoreInfoLocalFilePath error:nil];
        }
    }

    4、测试

    创建应用的沙盒测试账号

    这里写图片描述

    点击+号,根据具体信息填写。

    至此,整个流程结束。


    注: 完成购买的操作中,如果有服务器,需要向服务器验证购买结果,如果没有,就直接完成。

    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    调起支付后,输入注册的沙盒账号即可点击购买。

    注:这里有几个注意事项

    一,测试支付的ipa必须使用[App-Store]证书
    
    二,越狱机器无法测试IAP
    
    三,用SandBox账号测试支付的时候,必须把在系统[设置]里面把[Itunes Store 与 App Store]登录的非                               SandBox账号注销掉,否则向苹果服务器请求不到订单信息
    
    四,Sandbox账号不要在正式支付环境登陆支付,登陆过的正式支付环境的SandBox账号会失效
    
    五,所有在itunes上配置的商品都必须可购买,不能有某些商品根据商户自己的服务器的数据在某个时期出现免费的情况
    
    六,商品列表不能按照某些特定条件进行排序(比如说下载量)
    
    七,非消耗型商品必须的有恢复商品功能
    
    八,非消耗类型的商品不要和商户自己的服务器关联
    

    参考:

    https://www.jianshu.com/p/033086546126

    https://www.jianshu.com/p/e0ea5b8916f5

    http://openfibers.github.io/blog/2015/02/28/in-app-purchase-walk-through/

    http://www.himigame.com/iphone-cocos2d/550.html

    http://blog.devtang.com/2012/12/09/in-app-purchase-check-list/

    http://yarin.blog.51cto.com/1130898/549141

    更多技术文章,欢迎大家访问我的技术博客:https://blog.csdn.net/qcx321

    展开全文
  • 关于IAP的设置问题,网上其实已经写了很多了,我也不多赘述,那么我在这里只写一些细节,特别是对于新手来说的那种。 第一步肯定是去iTunes Connect里面添加项目,并且设置商品: 现在苹果其实已经不需要你上传ipa...

    参考:http://blog.csdn.net/pz789as/article/details/70208867

     

    关于IAP的设置问题,网上其实已经写了很多了,我也不多赘述,那么我在这里只写一些细节,特别是对于新手来说的那种。

    第一步肯定是去iTunes Connect里面添加项目,并且设置商品:

    现在苹果其实已经不需要你上传ipa包了,只要设置好了相关信息既可以测试iap功能。

    需要注意的是:“协议、税务和银行业务” 填写,这是最最重要的,如果没有填写,你永远不会收到结果,但是也不报错。

    税务信息如果填写完毕,那么应该是这样的:

    也就是后面可以download的

    在 功能 里面设置商品,你需要上传截图,不然是不会显示的,设置好之后,它们的状态应该是“准备提交”状态,说明这一步已经好了。然后还需要在APP Store信息中的初始版本里面选择你刚刚设置的商品,结果如下:

     

    另一个是沙盒测试账号,这个必须要使用你在iTunes Connect人员配置中设置的测试账号,否则是无法测试的。而且,在测试之前,一定要把设置里面的“iTunes Store与 App Store”退出。然后在游戏中点击购买的时候,会弹出一个登录账号的框,选择已经存在的账号,填写账号密码即可开始测试了。

    这是需要注意的三个重要点

     

    接下来就是unity这边了

    在unity5.5版本以后,你需要在创建项目的时候打开unity服务,不然后面去弄,比较麻烦,半天连不上。

    这个unity服务中,你找到In-App Purchasing,将他开启,然后import Unity的内购插件。

    这里需要说明的一点是关于Restore的使用方式,其实Unity已经做好了,不需要关心,我要说的是他的返回的方式。

    因为之前不知道,在看到Restore之后,返回只有一个bool类型的变量,所以不知道用户到底买了哪个非消费类型的商品。

    其实,Unity在返回bool类型之前,其实还是会回调购买成功的函数,然后参数带了商品的ID,如果有多个非消费类型商品,它会多次回调,这样你就可以根据ID去做你的处理了。

    对于其他的操作,unity的文档和案例已经写的很清楚了。

    另外,使用代码和使用IAPButton是一样的效果,我贴出使用代码的方法:

     

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Purchasing;
    using System;
    
    namespace IAPCustom
    {
    	//其实直接使用插件提供的IAPButton即可,不需要关心其他的
    public class UnityPurchaser : MonoBehaviour, IStoreListener {
    
    	private static IStoreController m_StoreController;
    	private static IExtensionProvider m_StoreExtensionProvider;
    	public static string kProductIDConsumable = "buycoins0";
    	public static string kProductIDNonConsumable = "removeads";
    	public static string kProductIDWeapon = "weapon";
    
    	public static string kProductNameAppleConsumable = "com.gjc.wf.buycoins0";
    	public static string kProductNameAppleNonConsumable = "com.gjc.wf.buyremoveads";
    	public static string kProductNameAppleWeapon = "com.gjc.wf.weapon";
    
    	public static string kProductIDSubscription = "subscription"; 
    	// Apple App Store-specific product identifier for the subscription product.
    	// private static string kProductNameAppleSubscription = "com.unity3d.subscription.new";
    	// Google Play Store-specific product identifier subscription product.
    	// private static string kProductNameGooglePlaySubscription = "com.unity3d.subscription.original";
    	void Start () {
    		if (m_StoreController == null){
    			InitializePurchasing();
    		}	
    	}
    	public void InitializePurchasing() 
    	{
    		if (IsInitialized())
    		{
    			return;
    		}
    		ConfigurationBuilder builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    
    		// builder.AddProduct(kProductIDConsumable, ProductType.Consumable, new IDs(){
    		// 	{kProductNameAppleConsumable, AppleAppStore.Name}
    		// });
    		// builder.AddProduct(kProductIDNonConsumable, ProductType.NonConsumable, new IDs(){
    		// 	{kProductNameAppleNonConsumable, AppleAppStore.Name}
    		// });
    		// builder.AddProduct(kProductIDSubscription, ProductType.Subscription, new IDs(){
    		// 	{ kProductNameAppleSubscription, AppleAppStore.Name },
    		// 	{ kProductNameGooglePlaySubscription, GooglePlay.Name },
    		// });
    
    		//添加商品
    		IDs kProductBuyCoins0 = new IDs();
    		IDs kProductRemoveAds = new IDs();
    		kProductBuyCoins0.Add(kProductNameAppleConsumable, new string[]{AppleAppStore.Name});
    		kProductRemoveAds.Add(kProductNameAppleNonConsumable, new string[]{AppleAppStore.Name});
    		builder.AddProduct(kProductIDConsumable, ProductType.Consumable, kProductBuyCoins0);
    		builder.AddProduct(kProductIDNonConsumable, ProductType.NonConsumable, kProductRemoveAds);
    		builder.AddProduct(kProductIDWeapon, ProductType.NonConsumable, new IDs(){
    			{kProductNameAppleWeapon, AppleAppStore.Name}
    		});
    		// ProductCatalog pc = ProductCatalog.LoadDefaultCatalog();
    		
    		UnityPurchasing.Initialize(this, builder);
    	}
    	
    	
    	private bool IsInitialized()
    	{
    		return m_StoreController != null && m_StoreExtensionProvider != null;
    	}
    	public void BuyConsumable()
    	{
    		BuyProductID(kProductIDConsumable);
    	}
    	public void BuyNonConsumable()
    	{
    		BuyProductID(kProductIDNonConsumable);
    	}
    	public void BuyWeapon(){
    		BuyProductID(kProductIDWeapon);
    	}
    	public void BuySubscription()
    	{
    		BuyProductID(kProductIDSubscription);
    	}
    	void BuyProductID(string productId)
    	{
    		if (IsInitialized())
    		{
    			Product product = m_StoreController.products.WithID(productId);
    			if (product != null && product.availableToPurchase)
    			{
    				Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));
    				m_StoreController.InitiatePurchase(product);
    			}
    			else
    			{
    				Debug.Log("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
    			}
    		}
    		else
    		{
    			Debug.Log("BuyProductID FAIL. Not initialized.");
    		}
    	}
    	public void RestorePurchases()
    	{
    		if (!IsInitialized())
    		{
    			Debug.Log("RestorePurchases FAIL. Not initialized.");
    			return;
    		}
    		if (Application.platform == RuntimePlatform.IPhonePlayer || 
    			Application.platform == RuntimePlatform.OSXPlayer)
    		{
    			Debug.Log("RestorePurchases started ...");
    			var apple = m_StoreExtensionProvider.GetExtension<IAppleExtensions>();
    			apple.RestoreTransactions((result) => {
    				//返回一个bool值,如果成功,则会多次调用支付回调,然后根据支付回调中的参数得到商品id,最后做处理(ProcessPurchase)
    				Debug.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
    			});
    		}
    		else
    		{
    			Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
    		}
    	}
    
    	//  
    	// --- IStoreListener
    	//
    	public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    	{
    		//初始化成功
    		Debug.Log("OnInitialized: PASS");
    		m_StoreController = controller;
    		m_StoreExtensionProvider = extensions;
    	}
    	public void OnInitializeFailed(InitializationFailureReason error)
    	{
    		//初始化失败
    		Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
    	}
    	public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args) 
    	{
    		//根据不同的id,做对应的处理。。
    		if (String.Equals(args.purchasedProduct.definition.id, kProductIDConsumable, StringComparison.Ordinal))
    		{
    			Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
    		}
    		else if (String.Equals(args.purchasedProduct.definition.id, kProductIDNonConsumable, StringComparison.Ordinal))
    		{
    			Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
    		}
    		else if (String.Equals(args.purchasedProduct.definition.id, kProductIDWeapon, StringComparison.Ordinal)){
    			Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
    		}
    		else if (String.Equals(args.purchasedProduct.definition.id, kProductIDSubscription, StringComparison.Ordinal))
    		{
    			Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
    		}
    		else 
    		{
    			Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
    		}
    		return PurchaseProcessingResult.Complete;
    	}
    	
    	public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    	{
    		//支付失败
    		Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
    	}
    }
    }

     

    两个方法使用其中一个即可。。

     

     

    下午补充:

    在后面的测试中发现,如果商品有非消费类型时,使用UnityIAP插件中自带的IAPButton组件时,会出现很奇怪的情况:

    在购买完非消费类型的商品后,然后删除APP,重新安装APP的时候,它会莫名其妙的自动调用Restore,这样等于我还没登录,它就使用原来的账号去获取检测是否购买。我查了半天也不知道哪里出的问题。但是当我不使用IAPButton,全部用自己代码去调用就不会出现这种情况。。

    不过没有非消费类型的商品还是可以用那个Button的!

    个人建议最好还是使用代码调用,代码已经在上面贴出来可~

     

    2017年11月26号补充:

    其实Unity给我们提供了CataLog,可以很方便的添加各个平台不同的id,而只使用同样的加载方式即可。

    下面给出获取Catalog数据方式的代码:(方便以后其他平台移植,到时候直接在catalog里面设置就好啦)

     

    public void InitializePurchasing() 
    	{
    		if (IsInitialized())
    		{
    			return;
    		}
    		StandardPurchasingModule module = StandardPurchasingModule.Instance();
    		module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
    		ConfigurationBuilder builder = ConfigurationBuilder.Instance(module);
    		//通过编辑器中的Catalog添加,方便操作
    		ProductCatalog catalog = ProductCatalog.LoadDefaultCatalog();
    		foreach (var product in catalog.allProducts) {
    			if (product.allStoreIDs.Count > 0) {
    				var ids = new IDs();
    				foreach (var storeID in product.allStoreIDs) {
    					ids.Add(storeID.id, storeID.store);
    				}
    				builder.AddProduct(product.id, product.type, ids);
    			} else {
    				builder.AddProduct(product.id, product.type);
    			}
    		}
    		UnityPurchasing.Initialize(this, builder);
    	}


    这样就可以不用管这边加载代码,其他地方都是通用的。哈哈哈

     

     

     

    2017.12.19日补充,发现好多新手小伙伴还是一脸懵逼,我今天把最新的方式发在这里,供大家参考:

    本次使用的还是和catalog结合使用,另外关于商品id还不清楚的我在这里再说一下。我这里有两个商品ID,第一个是Unity这边使用的ID,用于Unity端调用,也是用来映射不同平台商品id的自定义ID,他填的位置如下:

    这个id可以自定义,也可以用实际商店的id,只要是一个字符串就可以了。

    然后第二个ID,就是个个平台设置的商品ID了,他们可能会有各种格式,所以unity这边做了处理,第一个自定义的id就是多了一层映射,方便管理和使用,了解了这一层关系,就知道我为什么要推荐用catalog了。因为在移植到其他平台时,我只要维护catalog的的每个平台的id就可以了,代码那边都需要关心。

     

    明白这两个ID的作用,那么就可以直接看我的下面的代码了,我把ShopList.cs都贴出来,相信大家能明白其中的原由了。

     

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using UnityEngine.Purchasing;
    
    /// <summary>
    /// 这是通用方式,通过读取catalog里面的信息,获取所有商品信息
    /// </summary>
    public class ShopList : MonoBehaviour, IStoreListener{
    	private static IStoreController m_StoreController;
    	private static IExtensionProvider m_StoreExtensionProvider;
    	bool blnRestore = false;//用来表示
    	bool blnPressRestore = false;//用来区分是否按了	restore 按钮
    
    	int dataLen = 3;
    	//每个商品的内容(金币),价格(除以100),折扣
    	int[] shopData = new int[]{
    		150,99,0,	//0.99 美元购买 150 个金币
    		450,299,0,	//2.99 美元购买 450 个金币
    		850,499,12,	//4.99 美元购买 850 个金币
    		1850,999,22,	//9.99 美元购买 1850 个金币
    		3950,1999,30,	//19.99 美元购买 3950 个金币
    		0,199,0		//1.99 美元购买去广告功能,可以Restore的项目
    	};
    
    	//catalog 里面设置的id,和这边一一对应,这个id是unity端的一个映射,在catalog里面可以对应不同平台的真实的 商品id
    	private string[] kProducts = new string[] {
    		"buycoins0",
    		"buycoins1",
    		"buycoins2",
    		"buycoins3",
    		"buycoins4",
    		"removeads"
    	};
    	void Start () {
    		InitializePurchasing();
    	}
    	private bool IsInitialized()
    	{
    		return m_StoreController != null && m_StoreExtensionProvider != null;
    	}
    	//初始化内购项目,主要是从catalog中获取商品信息,设置给 UnityPurchasing
    	void InitializePurchasing() 
    	{
    		if (IsInitialized())
    		{
    			Debug.Log("初始化失败");
    			return;
    		}
    		StandardPurchasingModule module = StandardPurchasingModule.Instance();
    		module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
    		ConfigurationBuilder builder = ConfigurationBuilder.Instance(module);
    		//通过编辑器中的Catalog添加,方便操作
    		ProductCatalog catalog = ProductCatalog.LoadDefaultCatalog();
    		// Debug.Log(catalog.allProducts.Count);
    		foreach (var product in catalog.allProducts) {
    			if (product.allStoreIDs.Count > 0) {
    				// Debug.Log("product:" + product.id);
    				var ids = new IDs();
    				foreach (var storeID in product.allStoreIDs) {
    					ids.Add(storeID.id, storeID.store);
    					// Debug.Log("stordId:" + storeID.id  + ", " + storeID.store);
    				}
    				builder.AddProduct(product.id, product.type, ids);
    			} else {
    				builder.AddProduct(product.id, product.type);
    			}
    		}
    		UnityPurchasing.Initialize(this, builder);
    	}
    
    	//供外部调用,当按 Restore 按钮时触发
    	public void OnRestore(){
    		if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.WindowsEditor){
    			Debug.Log("Restore success!");
    		}else{
    			blnRestore = true;
    			RestorePurchases();
    			blnPressRestore = true;
    		}
    	}
    	//供外部调用,按下哪个按钮,就可以购买哪一档的金币,我这里是通过按钮的名称得到购买的 idx 的,可以根据自己需要更改,比如:OnBuyCoins(int idx)
    	//idx 是上面 shopData 对应的每行数据
    	public void OnBuyCoins(Button btn){
    		int idx = System.Convert.ToInt32(btn.name);
    		BuyCoinsWithIdx(idx);
    	}
    	//实际购买调用的函数,根据idx拿到unity端的商品id
    	void BuyCoinsWithIdx(int idx){
    		if (idx == 5){//购买去广告
    			if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.WindowsEditor){
    				Debug.Log("购买去广告!");
    			}else{
    				blnPressRestore = false;
    				BuyProductID(kProducts[idx]);
    			}
    		}else{
    			if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.WindowsEditor){
    				Debug.Log("editor buy coins");
    			}else{
    				BuyProductID(kProducts[idx]);
    			}
    		}
    	}
    	//这里是通过商品id购买物品
    	void BuyProductID(string productId)
    	{
    		if (IsInitialized())
    		{
    			Debug.Log("Buy ProductID: " + productId);
    			Product product = m_StoreController.products.WithID(productId);
    			if (product != null && product.availableToPurchase) {
    				Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));
    				m_StoreController.InitiatePurchase(product);
    			} else {
    				Debug.Log("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
    			}
    		}else {
    			Debug.Log("没出初始化");
    		}
    	}
    	//真是的发起Restore请求
    	public void RestorePurchases()
    	{
    		if (!IsInitialized()) {
    			Debug.Log("没出初始化");
    			return;
    		}
    		if (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.OSXPlayer) {
    			// Debug.Log("RestorePurchases started ...");
    			var apple = m_StoreExtensionProvider.GetExtension<IAppleExtensions>();
    			apple.RestoreTransactions(HandleRestored);
    		}else {
    			Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
    		}
    	}
    	//如果restore之后,会返回一个状态,如果状态为true,那边以前购买的非消耗物品都会回调一次 ProcessPurchase 然后在这里个回调里面进行处理
    	void HandleRestored(bool result){
    		//返回一个bool值,如果成功,则会多次调用支付回调,然后根据支付回调中的参数得到商品id,最后做处理(ProcessPurchase)
    		// Debug.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
    		blnRestore = false;
    		if (result){
    			Debug.Log("Restore success!");
    		}else{
    			Debug.Log("Restore Failed!");
    		}
    	}
    	//初始化回调
    	public void OnInitialized(IStoreController controller, IExtensionProvider extensions){
    		//初始化成功
    		Debug.Log("OnInitialized: PASS");
    		m_StoreController = controller;
    		m_StoreExtensionProvider = extensions;
    	}
    	public void OnInitializeFailed(InitializationFailureReason error){
    		//初始化失败
    		Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
    		if (error == InitializationFailureReason.AppNotKnown){
    			//
    		}else if (error == InitializationFailureReason.NoProductsAvailable){
    			//
    		}else if (error == InitializationFailureReason.PurchasingUnavailable){
    			//
    		}
    	}
    	//购买成功后的回调,包括restore的商品
    	public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args) 
    	{
    		// Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
    		//根据不同的id,做对应的处理。。
    		int key = -1;
    		for(int i=0;i<kProducts.Length;i++){
    			if (string.Equals(args.purchasedProduct.definition.id, kProducts[i], System.StringComparison.Ordinal)){
    				key = i;
    				break;
    			}		
    		}
    		if (key == -1){
    			Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
    		}else{
    			if (key == 5){
    				if (!blnPressRestore){
    					Debug.Log("Ads have been removed!");
    				}
    			}else{
    				Debug.Log("购买了"+shopData[key*dataLen].ToString()+"个金币");
    			}
    		}
    		return PurchaseProcessingResult.Complete;
    	}
    	//购买失败回调,根据具体情况给出具体的提示
    	public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    	{
    		//支付失败
    		Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
    		if (failureReason == PurchaseFailureReason.UserCancelled){
    			//用户取消交易
    		}else if (failureReason == PurchaseFailureReason.ExistingPurchasePending){
    			//上一笔交易还未完成
    		}else if (failureReason == PurchaseFailureReason.PaymentDeclined){
    			//拒绝付款
    		}else if (failureReason == PurchaseFailureReason.ProductUnavailable){
    			//商品不可用
    		}else if (failureReason == PurchaseFailureReason.PurchasingUnavailable){
    			//支付不可用
    		}else{
    			//位置错误
    		}
    	}
    }
    

     

     

     

     

     

    具体使用,就是把上面这个脚本挂载到一个场景物体上,然后在Button里面设置按钮事件,我现在的写法是根据按钮的名字来得到需要购买的商品序号,通过序号拿到商品的Unity端ID,最后就可以发起购买啦。

     

    当然,这个方式也可以根据自己的需求去改,比如传递一个固定序号等等,不需要用按钮名称。

     

    2019.3.18补充:

    使用了最新版本的UnityIAP插件,在使用Catalog初始化IAP时,可以用下面代码了,可以更加的方便,并且少了出错的几率,特别是自动恢复:

    void InitializePurchasing () {
    		if (IsInitialized ()) {
    			return;
    		}
    		StandardPurchasingModule module = StandardPurchasingModule.Instance ();
    		module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
    		ConfigurationBuilder builder = ConfigurationBuilder.Instance (module);
    		//通过编辑器中的Catalog添加,方便操作
    		ProductCatalog catalog = ProductCatalog.LoadDefaultCatalog ();
    		//新的方法,直接调用Unity给的解析函数去加载Catalog
    		IAPConfigurationHelper.PopulateConfigurationBuilder (ref builder, catalog);
    		UnityPurchasing.Initialize (this, builder);
    	}

    主要的新方法就是 IAPConfigurationHelper.PopulateConfigurationBuilder 了,是不是很方便啦~

     

    展开全文
  • 之前写的一篇文章内所用的插件没有productcongtent的返回,这样对于需要服务器进行验证的支付没办法使用,特此找了其他插件重新做。感谢原作者http://blog.sina.com.cn/s/blog_4a2183a60101lc8a.html
  • ios 苹果支付(IAP)

    2019-10-16 16:45:23
    什么是IAP,即in-app-purchase。(虚拟商品如:课程、视频、音乐等数字产品只能走apple 的内购),苹果要扣除30%的销售额,再扣除一点相关的交易税,用户到手将不到7成。 官方参考文档 内购流程 1.获取内购列表(从App...
  • IAP的功能实现了基本流程,还需要大部分的时间来处理异常操作。 连续多次点击购买 处理一:可以判断当前PaymentQueue中是否已经有了购买请求(是只当前未处理完成的) __block BOOL bFind = NO; [[...
  • 前言什么是IAP,即in-app-purchase 这几天一直在搞ios的应用内购,查了很多博客,发现几乎没有一篇博客可以完整的概括出所有的点,为了防止大伙多次查阅资料,所以写了这一篇博客,希望大家能够跟着我,从零开始,...
  • IAP支付(IOS内购)

    2020-07-30 23:32:18
    IOS内购(IAP支付)的C++封装,适用于Cocos2d-x开发。 教程地址:http://blog.csdn.net/ldpjay/article/details/46459509
  • iOS接入IAP教程

    2018-01-07 06:51:07
    什么是IAP,我们什么时候需要使用IAP IAP就是应用内购买的英文缩写,在我们应用中如果有虚拟货币(金币,欢乐豆,钻石,各种币),以及虚拟服务(VIP类,游戏的拓展包,内容的按月订阅,去广告之类的增值服务),...
  • unity与iOS交互进行App Store内购,适用于游戏开发进行打包iOS并发布到App Store中使用app内购不会的unity开发者,使用简单,方便。可以很大提高游戏开发者的效率。
  • 这次我们尝试做一个收费的产品,所以在iOS端集成了应用内支付(IAP)功能。在开发过程中和上线后,我们遇到了IAP中的一些坑,在此分享给各位。   IAP 审核相关的坑 IAP开发的详细步骤我写在另一篇博客中了。...
  • IOS IAP支付总结

    2014-10-16 18:07:52
    iap: 准备工作:1.首先要
  • iOS内购 IAP
  • iOS IAP的开发学习总结

    2012-10-22 09:14:13
    如何学习Apple的IAP,开发完毕后总结如下: 最好的学习资料是阅读苹果官方文档,深入浅出解释各种概念,还有Step By Step的教程 官方文档 ...
  • iOS应用内购买项目流程,大致分为三个部分: 第一部分:iTunes connect设置,包含创建新的发布版本号、创建购买项目(商品)、设置协议税务银行业务、添加沙盒技术测试账号; 第二部分:在项目Xcode中设置允许In-...
  • iOS IAP恢复

    2015-06-29 11:40:08
    //——2012-6-25日更新iap恢复 看到很多童鞋说让Himi讲解如何恢复iap产品,其实博文已经给出了。这里再详细说下: 首先向AppStore请求恢复交易: 1 [[SKPaymentQueue defaultQueue] ...
  • Unity iOS内购 思路: Unity调用iOS内购代码实现 效果图: 重要提示: 测试一定要用沙盒账号,否则无效! 流程 这里就不重复写了,直接上截图  OC代码: IAPInterface(主要是实现Unity跟OC的IAP代码的...
1 2 3 4 5 ... 20
收藏数 3,795
精华内容 1,518
关键字:

21002 iap ios