我最近正在制作的一个程序里面,我就决定先把程序免费(其中只包含一个故事),然后把更多的故事放在in-app purchase里面。在这篇教程里面,你将会学到如何使用程序内置付费来解琐本地程序里面的内容,我将向你展示一些技巧,用来应付使用程序内置购买功能时的一些异步特性。请谨慎采纳这些建议,因为我的程序也还在开发之中,但是,随着我的知识的积累,我会逐步更新教程内容以确保不误人子弟。

from raywenderlich.com
成为ios开发者最大的好处就是,你编写的应用程序会有很多方式可以赚钱。比如,收费版,免费挂广告版,还有就是程序内置购买。
程序内置购买会让你爱不释手,主要有以下原因:
  • 除了程序本身的下载收费以外,你还可以赚更多的钱。一些用户愿意为那些额外的功能花费大量的金钱。
  • 你可以免费发布你的程序(这样的话,用户就可以任意下载了),如果他们喜欢这个程序的话,那么就会有人愿意购买额外功能。
  • 在你做完一个程序的时候,你可以在以后的发布版中添加更多的功能,然后这些功能可以用内置购买,这样的话,你就不用再重新制作另一个程序了。
我最近正在制作的一个程序里面,我就决定先把程序免费(其中只包含一个故事),然后把更多的故事放在in-app purchase里面。
在这篇教程里面,你将会学到如何使用程序内置付费来解琐本地程序里面的内容,我将向你展示一些技巧,用来应付使用程序内置购买功能时的一些异步特性。请谨慎采纳这些建议,因为我的程序也还在开发之中,但是,随着我的知识的积累,我会逐步更新教程内容以确保不误人子弟。
这篇教程的前提条件你需要熟悉基本的ios编程概念,如果你还是一个ios开发新手,可以先参考这些教程。
In App Rage
那么,本教程将制作一个怎样的程序呢?好吧,在揭晓答案之前,我先介绍一些背景情况。。。
最近,我对 rage comics这玩意儿非常着迷。如果你以前从没听说过它,让我向你们介绍一下吧。它们实际上就是一些非常有趣的漫画,里面有些人非常搞笑和搞怪的人和事。
因此,这篇教程,我们想要做一个非常小巧的应用,叫做“In App Rage”,在这个程序里面,用户可以使用内置购买来获得一些漫画。但是,在我们开始编码之前,我们需要先用ios Developer Center和iTunes Connect来为本程序创建一个入口点。
第一步,就是为这个程序创建一个App ID。所以,首先登录 iOS Developer Center,选择“App IDs”标签而,然后点击“New App ID”,如下图所示:
你可以按照下面的截图,根据提示 输入描述和bundle identifier:
注意,你不能直接使用上面这个bundle identifier,你需要定义你自己的独一无二的identifier,通常的做法是把你的域名反过来写就行了,然后你也可以基于其它规则来制作啦。
当你完成的时候,点击Submit。好,恭喜你,你现在有一个新的App ID了!现在,你将使用这个ID在iTunes Connect里面来创建一个新的应用了。
首先登录 iTunes Connect,点击“Manage Your Applications”,然后选择“Add New App”,并输入依次App Name,SKU number,同时选择你之前刚刚创建好的Bundle ID。
你可能不得不在你的应用程序名字上面下点功夫,因为,app名字必须是唯一的,而且我们之前为它添加了一个入口点(entry)。
接下来的两页将要求你输入你的应用程序的一些信息。现在,可以随便填一些内容,因为后面还有机会再更改。但是,每个带×号的文本框你都必须要填好(包括程序截图,甚至你现在还没有截图,呵呵,造一个吧)
好吧,让你们看看我对于这个过程的感觉吧,请看下图:
如果你像上面一样出错了,只需要随便填写一些数据就可以了。你可以使用任何图标或者截屏,只要大小合适就行了。一旦你把所有的错误都解决完以后,你就大功告成啦,oh yeah!
管理In App Purchases
在你开始编写in app purchase代码之前,你需要为此创建一个桩应用(placeholder app),同时,你必须在iTunes Connet里面设置好。所以,现在你拥有一个桩应用了,你现在只需要点击“Manage In App Purchases”按钮就行了,如下图所示:
然后,点击左上角的“Create New”,然后按照下图所示,填写相应的信息:
让我们来解释下这几个文本域的含义吧:
  • Reference Name: 这个名字就是在使用in-app purchase的时候会显示在iTunes Connect里面。这个名字你可以随便取,因为在你的程序里面是看不到它滴。
  • Product ID: 在苹果的开发文档里面,这个也叫做“product identifier”,这是一个唯一的字符串,用来标识你的in-app purchase。通常的做法是,使用你的bundle id,然后在最后加一个唯一的字符串。
  • Type: 你可以选择non-consumable(购买一次,永久使用),comsumable(购买一次,使用一次),或者subscription(自动续款)。本教程中,我们采用non-consumables。
  • Cleared for Sale: 当应用程序上架的时候,程序内置购买功能购买记录清空。
  • Price Tier: 设置程序内置购买的价钱。
在你完成上面的设置以后,往下滚动鼠标,然后在Display Detail section部分添加一个English entry,如下图所示:
当你的程序的内置购买功能弄好之后,你查询App Store的时候会返回你刚刚设置的信息。
你可能会奇怪,为什么我们要设置刚刚这一步(毕竟,你还是可以直接硬编码在你的程序之中啊!)好吧,很明显Apple想知道你定的价钱嘛。同时,在App Store里面会根据你填写的这些东西来显示一些信息,比如,内置付费应用排行榜。最后,如果你这一步设置了,你之后会变得很轻松。因为,它让你不用硬编 码这些信息在你的代码之中。而且可以让你动态改变是允许内置购买还是禁止内置购买。
一旦你完成之后,保存entry,然后创建更多的实体(entry),和下面的截图效果类似。不要担心描述信息,我们并不会在本教程中使用它们。
你可能会注意到,这个过程要花费您不少时间,我能够相像,当你的程序有很多内置购买功能的时候,这个创建过程会有多么的烦人!幸运的是,本教程我们体会不到,但是,如果你的教程真的遇到了这种情况,呵呵,可以留言给我抱怨一下吧!:)
Retrieving Product List(提取产品列表)
在你能让用户从你的程序里面购买任何东西之前,你必须向iTunes Connect发送一个查询请求来从服务器上提取所有可用的产品列表。
我们可以直接在view controller里面添加代码来实现之,但是那样扩展性太不好了,不利于重用。所以,我们将创建一个辅助类来管理所有与in-app purchase相关的内容,然后你就可以在你的其它程序里面重用了。
在从服务器上获得产品列表的同时,这个辅助类还会记录哪些产品被购买了,哪些还没有。它会为每一个已经购买过的产品创建一个identifier,然后把它存到NSUserDefaults里面去。
好了,让我们动手实验一下吧!打开XCode,然后选择File\New Project,再选择 iOS\Application\Navigation-based Application,点击Choose。把工程命名为InAppRage,然后点击Save。
接下来,创建一个新的类来管理内置付费代码,命名为IAPHelper。首先,点击Classes分组,选择File\New File,然后是iOS\Cocoa Touch Class\Objective-C class,确保Subclass of NSObject被选中,然后点击Next。把这个文件命名为IAPHelper.m,通过确保“Also create IAPHelper.h” 被选中,然后点击Finish。
我们首先往IAPHelper.m里面添加一个方法来从iTunes Connect里面提取产品列表,代码如下:
  • - (void)requestProducts {
  • self.request = [[[SKProductsRequest alloc] initWithProductIdentifiers:_productIdentifiers] autorelease];
  • _request.delegate = self;
  • [_request start];
  • }
这个方法假设我们已经定义了一个实例变量,叫做 _productIdentifiers ,它包含了一串产品标识符,之后用来从iTunes Connect里面查询产品滴。(比如com.raywenderlich.inapprage.drummerrage)
它然后创建了一个SKProductsRequest实例,那是苹果公司写的一个类,它里面包含从iTunes Connect里面提取信息的代码。使用此类灰常easy,你只需要给它一个delegate(它必须符合 SKProductsRequestDelegate 协议),然后就可以调用start方法了。
我们设置IAPHelper类本身作为delegate,那就意味着此类会收到一个回调函数,此函数(productsRequest:didReceiveResponse)会返回产品列表。
Update: Jerry 在论坛里面指出,SKProductsRequestDelegate 协议是从SKRequestDelegate派生而来滴,而SKRequestDelegate协议有一个方法,叫 做 request:didFailWithError:。此方法会在失败的时候调用,如果你喜欢的话,你可以使用此方法来代码后面的timeout方 法。感谢Jerry!
好吧,接下来让我们来实现productsRequest:didReceiveResponse 方法吧,具体如下所示:
  • - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
  • NSLog(@"Received products results...");
  • self.products = response.products;
  • self.request = nil;
  • [[NSNotificationCenter defaultCenter] postNotificationName:kProductsLoadedNotification object:_products];
  • }
这个非常简单。它首先保存返回的产品列表(是一个SKProducts的数组),然后把request设置为nil(为了释放内存),然后发出一个通知,任何侦听这个通知的对象都会收到这个消息。
接下来添加初始化代码:
  • - (id)initWithProductIdentifiers:(NSSet *)productIdentifiers {
  • if ((self = [super init])) {
  • // Store product identifiers
  • _productIdentifiers = [productIdentifiers retain];
  • // Check for previously purchased products
  • NSMutableSet * purchasedProducts = [NSMutableSet set];
  • for (NSString * productIdentifier in _productIdentifiers) {
  • BOOL productPurchased = [[NSUserDefaults standardUserDefaults] boolForKey:productIdentifier];
  • if (productPurchased) {
  • [purchasedProducts addObject:productIdentifier];
  • NSLog(@"Previously purchased: %@", productIdentifier);
  • }
  • NSLog(@"Not purchased: %@", productIdentifier);
  • }
  • self.purchasedProducts = purchasedProducts;
  • }
  • return self;
  • }
这个初始化代码将检测哪些产品已经被购买,哪些还没有。通过查询NSUserDefaults可以知道,然后再建立一个适当的数据结构。
好了,现在,我们已经见过最重要的代码了。接下来,我们在头文件中添加一些声明。首先,打开 IAPHelper.h,并作如下修改:
  • #import <Foundation/Foundation.h>
  • #import "StoreKit/StoreKit.h"
  • #define kProductsLoadedNotification         @"ProductsLoaded"
  • @interface IAPHelper : NSObject <SKProductsRequestDelegate> {
  • NSSet * _productIdentifiers;
  • NSArray * _products;
  • NSMutableSet * _purchasedProducts;
  • SKProductsRequest * _request;
  • }
  • @property (retain) NSSet *productIdentifiers;
  • @property (retain) NSArray * products;
  • @property (retain) NSMutableSet *purchasedProducts;
  • @property (retain) SKProductsRequest *request;
  • - (void)requestProducts;
  • - (id)initWithProductIdentifiers:(NSSet *)productIdentifiers;
  • @end
这个简单地导入StoreKit 头文件,然后定义一些实例变量、函数和通知的名字。
接下来,在IAPHelper.m里面添加synthesize 代码,以后内存释放代码,如下所示:
  • // Under @implementation
  • @synthesize productIdentifiers = _productIdentifiers;
  • @synthesize products = _products;
  • @synthesize purchasedProducts = _purchasedProducts;
  • @synthesize request = _request;
  • // In dealloc
  • - (void)dealloc
  • {
  • [_productIdentifiers release];
  • _productIdentifiers = nil;
  • [_products release];
  • _products = nil;
  • [_purchasedProducts release];
  • _purchasedProducts = nil;
  • [_request release];
  • _request = nil;
  • [super dealloc];
  • }
最后一步,你需要添加StoreKit框架。右键点击Frameworks文件夹,然后点Add\Existing Frameworks ,然后选择 StoreKit.framework。然后选择Build\Build 编译一下,编译完之后,你的代码应该是没有错误的。(此方法在Xcode4.0以上不适用。4.0需要点击工程文件名,然后右键target,然后在 build phase里面添加框架)
Subclassing for Your App
这里将创建一个IAPHelper类,这样以后你在你的程序里面只需要继承一下它,然后指定你的产品标识符(product identifier)就可以啦。许多人给我提建议,说可以从WEB服务器上把产品标识符以及其它相关信息全部弄下来,然后,当你的应用程序需要更新的时 候,你就可以动态添加新的in-app purchase了。
这个提议非常好,但是,为了保持本教程的简单性,我这里就采用了硬编码的方式。
右键选中Classes 分组,然后选择File\New File,再选择 iOS\Cocoa Touch Class\Objective-C class,确保Subclass of NSObject 被复选中,然后点击Next。把这个文件命名为InAppRageIAPHelper.M,同时确保 “Also create InAppRageIAPHelper.h” 被复选中,然后点击Finish。
然后,把InAppRageIAPHelper.h 替换成下列代码:
  • #import <Foundation/Foundation.h>
  • #import "IAPHelper.h"
  • @interface InAppRageIAPHelper : IAPHelper {
  • }
  • + (InAppRageIAPHelper *) sharedHelper;
  • @end
这里把InAppRageIAPHelper类定义为IAPHelper类的子类,然后创建了一个静态方法用来创建些帮助类的单例。
接下来,把InAppRageIAPHelper.m替换成下面的代码:
  • #import "InAppRageIAPHelper.h"
  • @implementation InAppRageIAPHelper
  • static InAppRageIAPHelper * _sharedHelper;
  • + (InAppRageIAPHelper *) sharedHelper {
  • if (_sharedHelper != nil) {
  • return _sharedHelper;
  • }
  • _sharedHelper = [[InAppRageIAPHelper alloc] init];
  • return _sharedHelper;
  • }
  • - (id)init {
  • NSSet *productIdentifiers = [NSSet setWithObjects:
  • @"com.raywenderlich.inapprage.drummerrage",
  • @"com.raywenderlich.inapprage.itunesconnectrage",
  • @"com.raywenderlich.inapprage.nightlyrage",
  • @"com.raywenderlich.inapprage.studylikeaboss",
  • @"com.raywenderlich.inapprage.updogsadness",
  • nil];
  • if ((self = [super initWithProductIdentifiers:productIdentifiers])) {
  • }
  • return self;
  • }
  • @end
第一个sharedHelper方法是为了使InAppRageIAPHelper类变成一个单例类。注意,这种实现单例的方式并不是线程安全的,但是,对于本应用来说完全足够了,因为我们只有一个主线程。
接下来,我们硬编码了一组产品标识符的字符串数组,然后调用了基类的初始化方式。注意,我们在这里的字符串名字必须保持和之前在iTunes Connect里面定义的名称要一致。
然后选择Build\Build,保证没有错误再继续哦。
添加帮助类代码
我们差不多完成了我们的帮助类了,但是,在调用这个类的时候会有两个问题,我们接下来会讨论解决办法。
第一个问题就是,这段代码在没有网络连接的情况下是跑不起来滴。所以,我们在使用之前,需要检查是否有网络。
第二个问题,加载产品列表可以会耗费一定的时间,所以,我们需要让用户知道我们在加载产品列表,而不是神马都不显示,那样用户会以为程序出问题了。我们只需要简单的显示一个activity indicator就可以啦。
关于这两个问题,我们都可以自己动手来解决,但是,你为什么要重新发明轮子呢?(译者:工作中,遇到任何“问题”的时候,这里的“问题”,我指的是有点难度 的问题,或者自己一时想不清楚的问题,不要急着动手编码,你还没想清楚呢!瞎编码什么呀!不妨google一下,你会有意想不到的收获。当然,这里我并不 是鼓励大家不动脑筋,而是,有时候,我们程序员需要一种“懒”。)苹果已经为我们写好了一个检测网络是否可用的代码,叫做 Reachability class,而 Matej Bukovinski则为我们写了一个非常好用的指示器类 reusable progress indicator。你完全可以重用他们,而不要去重新发明轮子。
所以,尽管去下载这些源代码吧,当然,你也可以直接从本教程的源码中获得上面提到的源码。
一旦你下载完了这些文件,直接把MBProgressHUD.h/m 和 Reachability.h/m拖到你的项目的Classes分组下面就可以啦。同时确保 “Copy items into destination group’s folder”被复选中,然后点击Add。
最后一步----你需要添加SystemConfiguration 类库,因为Reachability这个类依赖此类库。右键点击Frameworks文件夹,然后选择Add\Existing Frameworks,然后再从列表中选择SystemConfiguration.framework就可以啦。然后,编译,确保没有错误后再继续。
好了,现在我们得到所有的产品列表和价格了,现在让我们把它们整合起来。
显示产品列表
打开RootViewController.h ,然后做如下修改:
  • // Before @interface
  • #import "MBProgressHUD.h"
  • // Inside @interface
  • MBProgressHUD *_hud;
  • // After @interface
  • @property (retain) MBProgressHUD *hud;
上面只是简单的声明一些实例变量和定义MBProgressHUD 属性。
然后,打开RootViewController.m,并做如下修改:
  • // At top of file
  • #import "InAppRageIAPHelper.h"
  • #import "Reachability.h"
  • // Under @implementation
  • @synthesize hud = _hud;
  • // Uncomment viewDidLoad and add the following
  • self.title = @"In App Rage";
  • // Uncomment viewWillAppear and add the following
  • self.tableView.hidden = TRUE;
  • [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(productsLoaded:) name:kProductsLoadedNotification object:nil];
  • Reachability *reach = [Reachability reachabilityForInternetConnection];
  • NetworkStatus netStatus = [reach currentReachabilityStatus];
  • if (netStatus == NotReachable) {
  • NSLog(@"No internet connection!");
  • } else {
  • if ([InAppRageIAPHelper sharedHelper].products == nil) {
  • [[InAppRageIAPHelper sharedHelper] requestProducts];
  • self.hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
  • _hud.labelText = @"Loading comics...";
  • [self performSelector:@selector(timeout:) withObject:nil afterDelay:30.0];
  • }
  • }
这里比较重要的代码在viewWillAppear里面。它首先设置table view默认情况下隐藏(table view在产品列表加载完之后会再重新显示滴)。然后,设置了一个通告,因为此类需要知道什么时候产品列表加载完了。
然后再使用Reachability 来检测网络是否可用。如果可用的话,它就调用IAPHelper的requestProducts 方法来下载之前填好的产品列表。
当产品列表在加载过程中的时候,我们用MBProgressHUD 显示一个“loading”界面。同时,我们还设置一个超时检测函数,当30秒过后,如果还没有加载完产品列表的话,我们就提示用户错误。
所以,接下来,让我们添加一些代码来处理通告消息,和超时处理函数。
  • - (void)dismissHUD:(id)arg {
  • [MBProgressHUD hideHUDForView:self.navigationController.view animated:YES];
  • self.hud = nil;
  • }
  • - (void)productsLoaded:(NSNotification *)notification {
  • [NSObject cancelPreviousPerformRequestsWithTarget:self];
  • [MBProgressHUD hideHUDForView:self.navigationController.view animated:YES];
  • self.tableView.hidden = FALSE;
  • [self.tableView reloadData];
  • }
  • - (void)timeout:(id)arg {
  • _hud.labelText = @"Timeout!";
  • _hud.detailsLabelText = @"Please try again later.";
  • _hud.customView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"37x-Checkmark.jpg"]] autorelease];
  • _hud.mode = MBProgressHUDModeCustomView;
  • [self performSelector:@selector(dismissHUD:) withObject:nil afterDelay:3.0];
  • }
第一个函数(dismissHUD)只是一个辅助函数,用来隐藏加载面板的。
第二个方法(productsLoaded)是在kProductsLoadedNotification 通告消息到达的时候被触发的。它隐藏了加载面板,同时重新加载table view里面的东西,用来显示down下来的产品列表滴。
最后一个方法(timeout),更新HUD并显示一个超时的消息,然后让这个HUD过一段时间再消失。
最后,我们需要在 RootViewController.m里面再添加一些代码来完成table view的动作,代码如下:
  • // Replare return 0 in numberOfRowsInSection with the following
  • return [[InAppRageIAPHelper sharedHelper].products count];
  • // In cellForRowAtIndexPath, change cell style to "subtitle":
  • cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
  • // In cellForRowAtIndexPath, under "Configure the cell"
  • SKProduct *product = [[InAppRageIAPHelper sharedHelper].products objectAtIndex:indexPath.row];
  • NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
  • [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
  • [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
  • [numberFormatter setLocale:product.priceLocale];
  • NSString *formattedString = [numberFormatter stringFromNumber:product.price];
  • cell.textLabel.text = product.localizedTitle;
  • cell.detailTextLabel.text = formattedString;
  • if ([[InAppRageIAPHelper sharedHelper].purchasedProducts containsObject:product.productIdentifier]) {
  • cell.accessoryType = UITableViewCellAccessoryCheckmark;
  • cell.accessoryView = nil;
  • } else {
  • UIButton *buyButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
  • buyButton.frame = CGRectMake(0, 0, 72, 37);
  • [buyButton setTitle:@"Buy" forState:UIControlStateNormal];
  • buyButton.tag = indexPath.row;
  • [buyButton addTarget:self action:@selector(buyButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
  • cell.accessoryType = UITableViewCellAccessoryNone;
  • cell.accessoryView = buyButton;
  • }
  • // In viewDidUnload
  • self.hud = nil;
  • // In dealloc
  • [_hud release];
  • _hud = nil;
在这里,table view只是简单的显示IAPHelper单例里面的产品列表---这个列表我们是通过SKProductsRequest来获取的。
products 数组里面的对象都是SKProduct的实例。它们包含了你在iTunes Connect里面设置的信息,比如title,description,price,etc.本教程中,table view只是简单的显示价格和标题。同时,我们还添加了一个“购买”按钮,现在这个“购买”还不起作用,因为我们还没有为它编码任何代码。
你现在差不多可以测试一下了,但是,还有最后一件步(而且是非常重要的一步!)。你需要设置bundle identifier。点击你的InAppRage-Info.plist并修改Bundle identifier来匹配你的ios Developer Center里面的那个,如下图所示:
好了,差不多了!编译并运行你的程序(你需要编译到设备上面,模拟器上是不行的),然后你会看到一个loading indicator,之后,就会显示一系列产品列表,如下图所示:
给我钱看看
这是篇超级无敌又臭又长的教程,而且最重要的部分还是没有讲到---如何处理支付,如何赚钱,接下来,马上为您揭晓!
做支付基本的几个要领如下:
  • 你创建一个SKPayment对象,然后指定用户想要购买的产品的标识符。然后把它加到支付队列(payment queue)里面去。
  • StoreKit将会提醒用户“are you sure?”, 然后要求用户输入用户名和密码,然后支付,然后就会返回给你,支付成功还是失败。你也可以处理这种情况:用户已经为此付过费了,然后可以重新再下载,同时给出一个恰当的提示就可以了。
  • 你 设计一个特殊的对象来处理支付通告回调消息。这个对象需要处理支付内容下载(在我们这个教程没必要,因为我们是硬编码的),同时解琐程序里面的相关内容 (我们可以通过使用NSUserDefaults类来处理,然后把值设置到purchasedProducts 里面就行啦)
不要担心---当你看到代码的时候,就会发现,其实这个过程是很easy滴。再强调一次,我们为了使IAPHelper尽可能可以重用,我们将在 IAPHelper.h里面做如下修改:
  • // Add two new notifications
  • #define kProductPurchasedNotification       @"ProductPurchased"
  • #define kProductPurchaseFailedNotification  @"ProductPurchaseFailed"
  • // Modify @interface to add the SKPaymentTransactionObserver protocol
  • @interface IAPHelper : NSObject <SKProductsRequestDelegate, SKPaymentTransactionObserver> {
  • // After @interface, add new method decl
  • - (void)buyProductIdentifier:(NSString *)productIdentifier;
然后打开IAPHelper.m 文件并作如下修改:
  • - (void)recordTransaction:(SKPaymentTransaction *)transaction {
  • // Optional: Record the transaction on the server side...
  • }
  • - (void)provideContent:(NSString *)productIdentifier {
  • NSLog(@"Toggling flag for: %@", productIdentifier);
  • [[NSUserDefaults standardUserDefaults] setBool:TRUE forKey:productIdentifier];
  • [[NSUserDefaults standardUserDefaults] synchronize];
  • [_purchasedProducts addObject:productIdentifier];
  • [[NSNotificationCenter defaultCenter] postNotificationName:kProductPurchasedNotification object:productIdentifier];
  • }
  • - (void)completeTransaction:(SKPaymentTransaction *)transaction {
  • NSLog(@"completeTransaction...");
  • [self recordTransaction: transaction];
  • [self provideContent: transaction.payment.productIdentifier];
  • [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
  • }
  • - (void)restoreTransaction:(SKPaymentTransaction *)transaction {
  • NSLog(@"restoreTransaction...");
  • [self recordTransaction: transaction];
  • [self provideContent: transaction.originalTransaction.payment.productIdentifier];
  • [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
  • }
  • - (void)failedTransaction:(SKPaymentTransaction *)transaction {
  • if (transaction.error.code != SKErrorPaymentCancelled)
  • {
  • NSLog(@"Transaction error: %@", transaction.error.localizedDescription);
  • }
  • [[NSNotificationCenter defaultCenter] postNotificationName:kProductPurchaseFailedNotification object:transaction];
  • [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
  • }
  • - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
  • {
  • for (SKPaymentTransaction *transaction in transactions)
  • {
  • switch (transaction.transactionState)
  • {
  • case SKPaymentTransactionStatePurchased:
  • [self completeTransaction:transaction];
  • break;
  • case SKPaymentTransactionStateFailed:
  • [self failedTransaction:transaction];
  • break;
  • case SKPaymentTransactionStateRestored:
  • [self restoreTransaction:transaction];
  • default:
  • break;
  • }
  • }
  • }
  • - (void)buyProductIdentifier:(NSString *)productIdentifier {
  • NSLog(@"Buying %@...", productIdentifier);
  • SKPayment *payment = [SKPayment paymentWithProductIdentifier:productIdentifier];
  • [[SKPaymentQueue defaultQueue] addPayment:payment];
  • }
啊!好多代码啊,但是,其实都不难,我会一个个向大家解释清楚。
当table view里面的buy按照被按下去的时候,它将会调用buyProductIdentifier函数。然后会创建一个新的SKPayment 对象,并且把这个对象加载到队伍中去。我们将把此类当作delegate来接收支持事务的更新消息,所以,当支付完成 的时候或者失败的时候,paymentQueue:updatedTransactions 这个函数将会被调用。
如果支付成功了(或者取消了),那么provideContent 函数都会被调用。然后,重点来了---它会在NSUserDefaults里面设置一个标记,然后把这个事务加到队列中去。剩下的代码就是用来检测用户是否获得了相应的内容了。
假如支付失败了,也会相应的有一个失败的通告消息会到达的。
注意,这里recordTransaction 并没有任何实现。如果你可以的话,你可以去实现此方法,然后给WEB服务器发送一个消息,让服务器来做一些记录。个人来讲,我觉得实现这个方法没什么实际的用处。
同时,也请注意,这种方法保存支付信息是非常容易被黑的(你需要加密保存),但是,我并不是很关心这个东东,因为,任何想要破解我的程序的人,他们肯定是不愿意付钱的,in-app对他们来说没什么意义。
在我们使用这些代码之前,我们还需要在App Delegate里面添加一些东西,这样的话,当产品支付事务完成的时候,IAPHelper类就会得到相应的通千。所以,打开InAppRageAppDelegate.m并作如下修改:
  • // At top of file
  • #import "InAppRageIAPHelper.h"
  • // In application:didFinishLaunchingWithOptions
  • [[SKPaymentQueue defaultQueue] addTransactionObserver:[InAppRageIAPHelper sharedHelper]];
如果没有这句代码的话,那么 paymentQueue:updatedTransactions 这个函数将不会被调用,所以,造成记得要加上去!
最后一步,让我们回到table view上面来。打开RootViewController.m ,然后作如下修改:
  • // Add new method
  • - (IBAction)buyButtonTapped:(id)sender {
  • UIButton *buyButton = (UIButton *)sender;
  • SKProduct *product = [[InAppRageIAPHelper sharedHelper].products objectAtIndex:buyButton.tag];
  • NSLog(@"Buying %@...", product.productIdentifier);
  • [[InAppRageIAPHelper sharedHelper] buyProductIdentifier:product.productIdentifier];
  • self.hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
  • _hud.labelText = @"Buying fable...";
  • [self performSelector:@selector(timeout:) withObject:nil afterDelay:60*5];
  • }
  • // Add inside viewWillAppear
  • [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(productPurchased:) name:kProductPurchasedNotification object:nil];
  • [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector(productPurchaseFailed:) name:kProductPurchaseFailedNotification object: nil];
  • // Add new methods
  • - (void)productPurchased:(NSNotification *)notification {
  • [NSObject cancelPreviousPerformRequestsWithTarget:self];
  • [MBProgressHUD hideHUDForView:self.navigationController.view animated:YES];
  • NSString *productIdentifier = (NSString *) notification.object;
  • NSLog(@"Purchased: %@", productIdentifier);
  • [self.tableView reloadData];
  • }
  • - (void)productPurchaseFailed:(NSNotification *)notification {
  • [NSObject cancelPreviousPerformRequestsWithTarget:self];
  • [MBProgressHUD hideHUDForView:self.navigationController.view animated:YES];
  • SKPaymentTransaction * transaction = (SKPaymentTransaction *) notification.object;
  • if (transaction.error.code != SKErrorPaymentCancelled) {
  • UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:@"Error!"
  • message:transaction.error.localizedDescription
  • delegate:nil
  • cancelButtonTitle:nil
  • otherButtonTitles:@"OK", nil] autorelease];
  • [alert show];
  • }
  • }
你就要成功啦,再坚持一小会儿!
In App Purchases, Accounts, and the Sandbox
当你在XCODE里面运行你的程序的时候,你并不是在运行真正的In-App Purchase服务器---你实际上是跑在沙盒服务器上面。
这意味着,你可以购买任何东西而不用担心会被扣钱。但是,你需要先创建一个测试帐号,同时确保你的设备登出了apple store,这样的话,你就可以看到这个处理过程了。
要创建测试帐号,你可以先登际 iTunes Connect ,然后点击“Manage Users”.点击“Test User”, 然后就可以创建一个测试帐号了。
然后,打开你的iphone,确保你退出当前的帐号了。你可以通过打开Settings程序,然后点击"Store",然后点"Sign out”。(大家千万注意啊!)
最后,运行你的程序吧。然后点击购买,输入测试帐号信息,如果一切顺利的话,你会得到如下截屏的输出!
但是,等一分钟---哪有里漫画啊!!!!你没值钱当然就没有啦。。。
好吧,这篇教程已经足够长了,用户购买以后可以得到漫画的任务就交由读者来完成吧。
何去何从?
本项目完整源代码:sample project
如果你想学习更多关于程序内置购买的内容,请参考苹果的文档 In-App Purchase Programming Guide。
同时,也请留意Noel Llopis 写的一些非常不错的文章。
收藏
 
回复

使用道具 举报

 
给帮你解决问题的大神点个‘膜拜’吧,点击“评分”->选择"膜拜值",即可膜拜大神,利人利己,还不扣自己分数的哦,亲!点我查看膜拜值兑换泰然币方式!!
lareina

沙发

  楼主| 发表于 2014-7-23 11:19:45 | 只看该作者

iOS应用内置付费 In-App Purchase 详细介绍

本帖最后由 lareina 于 2014-7-23 11:22 编辑

Store Kit代表App和App Store之间进行通信。程序将从App Store接收那些你想要提供的产品的信息,并将它们显示出来供用户购买。当用户需要购买某件产品时,程序调用StoreKit来收集购买信息。
In App Purchase(程序内购买)为苹果开发人员们打开了一个新的盈利渠道,如果您对此并不了解,下面这段 CocoaChina 会员“leon”翻译的 In App Purchase 详细介绍一定不能错过。
一、In App Purchase概览
Store Kit代表App和App Store之间进行通信。程序将从App Store接收那些你想要提供的产品的信息,并将它们显示出来供用户购买。
当用户需要购买某件产品时,程序调用StoreKit来收集购买信息。下图即为基本的store kit 模型:
Store Kit的API只是为程序添加In App Purchase功能的一小部分。你需要决定如何去记录那些你想要提交的产品,如何在程序中将商店功能展现给用户,还要考虑如何将用户购买的产品提交。本章的剩余部分会展示整个流程。
Products
产品可以是任意一项你想要出售的特性。产品在iTunes Connect中被组织,这和你添加一个新的App是一样的。支持的产品种类共有四种:
1. 内容型。包括电子书,电子杂志,照片,插图,游戏关卡,游戏角色,和其他的数字内容。
2. 扩展功能。这些功能已经包含在App内部。在未购买之前被锁定。例如,你可以在一个游戏程序中包含若干个小游戏,用户可以分别来购买这些游戏。
3. 服务。允许程序对单次服务收费。比如录音服务。
4. 订阅。支持对内容或服务的扩展访问。例如,你的程序可以每周提供财务信息或游戏门户网站的信息。应该设定一个合理的更新周期,以避免过于频繁的提示困扰用 户。要记住:你将负责跟踪订阅的过期信息,并且管理续费。App Store不会替你监视订阅的周期,也不提供自动收费的机制。
In App Purchase为创建产品提供了一种通用的机制,如何操作将由你负责。当你设计程序的时候,有以下几点需要注意:
1. 你必须提供电子类产品和服务。不要使用In App Purchase 去出售实物和实际服务。
2. 不能提供代表中介货币的物品,因为让用户知晓他们购买的商品和服务是很重要的。
通过App Store注册产品
每个你想要出售的产品都必须先通过iTunes Connect在App Store注册。你需提供产品的名称,描述,价格和其他在程序中用到的元数据。
需为产品指定唯一的标识符。当你的程序利用Store Kit和App Store通信时,会使用产品标识来取回产品的信息。如果用户购买某个商品时,程序可以用该标识来将产品标注为“已购买”。
App Store将前面提到过的产品种类简化为以下三种:
1. 消耗性商品。 该类商品在需要时被单次购买。比如,单次服务。
2. 非消耗性商品。 该类商品只需被某个用户购买一次,一旦被购买,和该用户iTunes 账户关联的设备都可以使用此商品。Store Kit为在多个设备上重新存储非消耗性商品提供了内置的支持。
3. 订阅类。订阅类商品拥有以上两种类型的特性。和消耗性商品一样,订阅类商品可以被多次购买; 你可以在程序内部加入自己的订阅计划更新机制。 另外,订阅类商品必须提供给和某一用户关联的所有设备。In App Purchase期望订阅类商品可以通过外部服务器交付。你必须为多个设备的订阅服务提供相应的支持。
关于注册产品的详细信息,请参考 iTunes Connect Developer Guide 文档。
交付方式
交付机制在程序In App Purchase的设计和实现种有很重要的意义。有两种基本的模型可以用来交付产品:内置类型(Built-in model)和服务器类型(Server model)。 不管使用那种模型,你都需要维护产品列表,并保证当用户购买后,成功的交付产品。
(一)内置产品类型
使用这种模型。 需要交付的产品已经在程序内部。 这种方式通常用在一些被锁定的功能上。 也可以用来交付在程序束(App Bundle)中的内容。 该方式的一个重要的优点是你可以及时的给客户交付产品,大多数的内置产品应为非消耗性商品。
注意:In App Purchase不提供购买补丁的功能。 如果需要更改app的bundle,你必须向App Store提交新的app版本。
为了标识产品,程序要在bundle中存储产品的标识符。内置模式下,Apple建议使用plist来纪录产品的标识符。 内容类应用可以使用折衷方式很方便的添加新的内容,而不改动程序本身。(原话为: Content-driven applications can use this to add new content without modifying the source for your application,不是很懂,感觉应该是说类似是用plist来管理产品列表,因此就不需要在添加新产品的时候改动程序了。再议。。。)
当成功购买产品后,程序应将锁定的功能解锁,提供给用户。 解锁的最简单方式是修改程序偏好设置(Application Preferences)。 当用户备份手机数据的时候,程序偏好设置也会随之备份。 程序可能需要建议用户在购买产品后备份手机以免丢失购买的内容。

 

上图显示了交付内置型产品的流程。
from mobile.51cto.com
1. 程序通过bundle存储的plist文件得到产品标识符的列表。
2. 程序向App Store发送请求,得到产品的信息。
3. App Store返回产品信息。
4. 程序把返回的产品信息显示给用户(App的store界面)
5. 用户选择某个产品
6. 程序向App Store发送支付请求
7. App Store处理支付请求并返回交易完成信息。
8. App获取信息并提供内容给用户。
(二)服务器类型
使用这种方式,要提供另外的服务器将产品发送给程序。 服务器交付适用于订阅、内容类商品和服务,因为商品可以作为数据发送,而不需改动程序束。 例如,一个游戏提供的新的内容(关卡等)。 Store Kit不会对服务器端的设计和交互做出定义,这方面工作需要你来完成。 而且,Store Kit不提供验证用户身份的机制,你需要来设计。 如果你的程序需要以上功能,例如,纪录特定用户的订阅计划, 你需要自己来设计和实现。

 

服务器类型的购买过程
1. 程序向服务器发送请求,获得一份产品列表。
2. 服务器返回包含产品标识符的列表。
3. 程序向App Store发送请求,得到产品的信息。
4. App Store返回产品信息。
5. 程序把返回的产品信息显示给用户(App的store界面)
6. 用户选择某个产品
7. 程序向App Store发送支付请求
8. App Store处理支付请求并返回交易完成信息。
9. 程序从信息中获得数据,并发送至服务器。
10. 服务器纪录数据,并进行审(我们的)查。
11. 服务器将数据发给App Store来验证该交易的有效性。
12. App Store对收到的数据进行解析,返回该数据和说明其是否有效的标识。
13. 服务器读取返回的数据,确定用户购买的内容。
14. 服务器将购买的内容传递给程序。
Apple建议在服务器端存储产品标识,而不要将其存储在plist中。 这样就可以在不升级程序的前提下添加新的产品。
在服务器模式下, 你的程序将获得交易(transaction)相关的信息,并将它发送给服务器。服务器可以验证收到的数据,并将其解码以确定需要交付的内容。 这个流程将在“验证store收据”一节讨论。
对于服务器模式,我们有安全性和可靠性方面的顾虑。 你应该测试整个环境来避免威胁。《Secure Coding Guide》文档中有相关的提示说明。
虽然非消耗性商品可以用内置模式来恢复,订阅类商品必须通过服务器来恢复。你要负责纪录订阅信息、恢复数据。 消耗类商品也可以通过服务器方式来纪录。例如,由服务器提供的一项服务, 你可能需要用户在多个设备上重新获得结果。

 
 
回复 支持 反对

举报

   
lareina

板凳

  楼主| 发表于 2014-7-23 11:25:08 | 只看该作者

iOS应用内付费(IAP)开发步骤列表

本帖最后由 lareina 于 2014-7-23 11:26 编辑

登录到Developer.apple.com,然后进行以下步骤,为应用建立建立一个不带通配符的App ID,用该App ID生成和安装相应的Provisioning Profile文件。

前两天和服务端同事一起,完成了应用内付费(以下简称IAP, In app purchase)的开发工作。步骤繁多,在此把开发步骤列表整理如下。因为只是步骤列表,所以并不含详细的说明教程,需要看教程的新手,可以看我附在最后的一些参考链接。
配置Developer.apple.com
登录到Developer.apple.com,然后进行以下步骤:
  • 为应用建立建立一个不带通配符的App ID
  • 用该App ID生成和安装相应的Provisioning Profile文件。
配置iTunes Connect
登录到iTunes Connet,然后进行以下步骤:
1.用该App ID创建一个新的应用。
2.在该应用中,创建应用内付费项目,选择付费类型,通常可选的是可重复消费(Consumable)的或是永久有效(Non-Consumable)的2 种,然后设置好价格和Product ID以及购买介绍和截图即可,这里的Product ID是需要记住的,后面开发的时候需要。如下图所示:
                                                                    from mobile.51cto.com
3.添加一个用于在sandbox付费的测试用户,如下图所示。注意苹果对该测试用户的密码要求 和正式账号一样,必须是至少8位,并且同时包含数字和大小写字母:
4.填写相关的税务,银行,联系人信息。如下图所示:
开发工作(ios端)
1、 在工程中引入 storekit.framework 和 #import <StoreKit/StoreKit.h>
2、 获得所有的付费Product ID列表。这个可以用常量存储在本地,也可以由自己的服务器返回。
3、 制作一个界面,展示所有的应用内付费项目。这些应用内付费项目的价格和介绍信息可以是自己的服务器返回。但如果是不带服务器的单机游戏应用或工具类应用, 则可以通过向App Store查询获得。我在测试时发现,向App Store查询速度非常慢,通常需要2-3秒钟,所以不建议这么做,最好还是搞个自己的服务器吧。
4、当用户点击了一个IAP项目,我们先查询用户是否允许应用内付费,如果不允许则不用进行以下步骤了。代码如下:
  • if ([SKPaymentQueue canMakePayments]) {
  • // 执行下面提到的第5步:
  • [self getProductInfo];
  • } else {
  • NSLog(@"失败,用户禁止应用内付费购买.");
  • }
5、 我们先通过该IAP的ProductID向AppStore查询,获得SKPayment实例,然后通过SKPaymentQueue的 addPayment方法发起一个购买的操作。
  • // 下面的ProductId应该是事先在itunesConnect中添加好的,已存在的付费项目。否则查询会失败。
  • - (void)getProductInfo {
  • NSSet * set = [NSSet setWithArray:@[@"ProductId"]];
  • SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
  • request.delegate = self;
  • [request start];
  • }
  • // 以上查询的回调函数
  • - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
  • NSArray *myProduct = response.products;
  • if (myProduct.count == 0) {
  • NSLog(@"无法获取产品信息,购买失败。");
  • return;
  • }
  • SKPayment * payment = [SKPayment paymentWithProduct:myProduct[0]];
  • [[SKPaymentQueue defaultQueue] addPayment:payment];
  • }
6、 在viewDidLoad方法中,将购买页面设置成购买的Observer。
  • - (void)viewDidLoad {
  • [super viewDidLoad];
  • // 监听购买结果
  • [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
  • }
  • - (void)viewDidUnload {
  • [super viewDidUnload];
  • [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
  • }
7、 当用户购买的操作有结果时,就会触发下面的回调函数,相应进行处理即可。
  • - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
  • for (SKPaymentTransaction *transaction in transactions)
  • {
  • switch (transaction.transactionState)
  • {
  • case SKPaymentTransactionStatePurchased://交易完成
  • NSLog(@"transactionIdentifier = %@", transaction.transactionIdentifier);
  • [self completeTransaction:transaction];
  • break;
  • case SKPaymentTransactionStateFailed://交易失败
  • [self failedTransaction:transaction];
  • break;
  • case SKPaymentTransactionStateRestored://已经购买过该商品
  • [self restoreTransaction:transaction];
  • break;
  • case SKPaymentTransactionStatePurchasing:      //商品添加进列表
  • NSLog(@"商品添加进列表");
  • break;
  • default:
  • break;
  • }
  • }
  • }
  • - (void)completeTransaction:(SKPaymentTransaction *)transaction {
  • // Your application should implement these two methods.
  • NSString * productIdentifier = transaction.payment.productIdentifier;
  • NSString * receipt = [transaction.transactionReceipt base64EncodedString];
  • if ([productIdentifier length] > 0) {
  • // 向自己的服务器验证购买凭证
  • }
  • // Remove the transaction from the payment queue.
  • [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
  • }
  • - (void)failedTransaction:(SKPaymentTransaction *)transaction {
  • if(transaction.error.code != SKErrorPaymentCancelled) {
  • NSLog(@"购买失败");
  • } else {
  • NSLog(@"用户取消交易");
  • }
  • [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
  • }
  • - (void)restoreTransaction:(SKPaymentTransaction *)transaction {
  • // 对于已购商品,处理恢复购买的逻辑
  • [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
  • }
8、服务器验证凭证(Optional)。如果购买成功,我们需要将凭证发送到服务器上进行验证。考虑到网络异常情况,iOS端的发送凭证操作应该进行持久化,如果程序退出,崩溃或网络异常,可以恢复重试。
开发工作(服务端)
服务端的工作比较简单,分4步:
  • 接收ios端发过来的购买凭证。
  • 判断凭证是否已经存在或验证过,然后存储该凭证。
  • 将该凭证发送到苹果的服务器验证,并将验证结果返回给客户端。
  • 如果需要,修改用户相应的会员权限。
考虑到网络异常情况,服务器的验证应该是一个可恢复的队列,如果网络失败了,应该进行重试。
与苹果的验证接口文档在这里。简单来说就是将该购买凭证用Base64编码,然后POST给苹果的验证服务器,苹果将验证结果以JSON形式返回。
苹果AppStore线上的购买凭证验证地址是https://buy.itunes.apple.com/verifyReceipt ,测试的验证地址是:https://sandbox.itunes.apple.com/verifyReceipt

 
 
回复 支持 反对

举报

   
lareina

地板

  楼主| 发表于 2014-7-23 11:27:56 | 只看该作者

关于IAP防止破解的几点

使用ECPurchase,verifyRecepitMode使用ECVerifyRecepitModeiPhone,会和apple的服务 器再做一次验证,负作用是可能购买时间稍长一些。不要是使用ECVerifyRecepitModeNone模式。然后 ECVerifyRecepitModeServer模式,在ECPurchase里面和ECVerifyRecepitModeiPhone一样的作 用,没有经过自己的服务器验证,需要重写。

1 推荐大家使用ECPurchase,verifyRecepitMode使用ECVerifyRecepitModeiPhone,会和apple的服务 器再做一次验证,负作用是可能购买时间稍长一些。不要是使用ECVerifyRecepitModeNone模式。然后 ECVerifyRecepitModeServer模式,在ECPurchase里面和ECVerifyRecepitModeiPhone一样的作 用,没有经过自己的服务器验证,需要重写,参考3。
2 破解的原理,这里就不讨论了,根源是Objective-C的runtime的副作用。
3 联网游戏,关键数据应该存储在服务器,然后将apple服务器的json数据发给自己服务器,再发到apple服务器做验证。验证失败后,你可以任意处理这个用户了。
4 非联网游戏,先做好第1点吧,有精力的可以参考第3点。不能根治,就是因为2了。
附上服务器端验证,php的代码:
  • <?php
  • public function recharge($params){
  • $url = "https://buy.itunes.apple.com/verifyReceipt";
  • $receipt = json_encode(array("receipt-data" => $base_key));
  • $response_json = $this->getHeader($url, $receipt);
  • $response = json_decode($response_json['content'], true);
  • if($response['status'] == 0){
  • //修改虚拟货币道具
  • }
  • }
  • /**
  • * 发送请求
  • */
  • protected function getHeader($url, $data){
  • $ch = curl_init();
  • $timeout = 300; // set to zero for no timeout
  • curl_setopt($ch, CURLOPT_URL, $url);
  • //       curl_setopt($ch, CURLOPT_ENCODING, 'gzip');
  • curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 1); //post到https
  • curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  • curl_setopt($ch, CURLOPT_POST, true);
  • curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
  • curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  • curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);//跟随页面的跳转
  • //       curl_setopt($ch, CURLOPT_HEADER, true);
  • curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
  • $handles = curl_exec($ch);
  • $header = curl_getinfo($ch);
  • curl_close($ch);
  • $header['content'] = $handles;
  • return $header;
  • }
  • ?>
                                                                                 from mobile.51cto.com
附件ECPurchase的zip包下载地址:http://down.51cto.com/data/937473

 
 
回复 支持 反对

举报

   
lareina

5#

  楼主| 发表于 2014-7-23 11:32:30 | 只看该作者

为你的应用添加内置收费 In App Purchase

本帖最后由 lareina 于 2014-7-23 11:45 编辑

本文讲阐述如何为你现有的应用或游戏添加程序内购买(In App Purchase)功能。阅读前提:假设了你拥有一定的iPhone SDK开发基础,和App Store实践经验。
自从09年10月16日苹果发送给所有开发者的那一封信,通知了程序内购买将可用于免费应用,我认为这也宣告了pTE版的时代结束。
成千上万的免费版的软件或游戏,将作为限制了功能的完整版免费发售,如果用户觉得有意购买,可以直接解锁完整版的功能或购买附加内容。
本文讲阐述如何为你现有的应用或游戏添加程序内购买(In App Purchase)功能。
阅读前提:假设了你拥有一定的iPhone SDK开发基础,和App Store实践经验。
你的程序需要唯一的Bundle ID, 如果已经发布的程序使用了通配符,那么就不能直接添加该功能。如果拥有了唯一的ID,请在program portal中添加你的app id,允许程序使用内购买(Enable In App Purchase)。
在设计好你的程序内购买的功能以后,首先需要定义元数据。来到iTunes Connect,在Manage Your In App Purchases中为你的程序添加一个程序内购买的商品。

 

from mobile.51cto.com
上面第一个部分是添加的内部信息。参考名称,ID,类型,和价格。以及各种本地化信息。
这里简单说一下类型的选择,其他信息,以及In App Purchase支持什么类型的商品请查看Getting Started with In App Purchase。
Non-consumable 默认类型,非消耗品,具有持久性,特点是一次性购买,可用在所有设备。例如解锁游戏的储存关卡功能。
Consumable 消耗品,用完或使用后消失,可多次购买,不能用在所有的设备,例如游戏中的血瓶或技能书。
Subscription 订阅,和消耗品一样,除了可以在设备之间共享。
创建购买流程
大致可以分为十个步骤:
验证App Store存取权限 这样做是因为,家长控制或一些公司的配置文件阻止了此功能,使用支付队列的查询功能canMakePayments来确定用户是否有权限购买,返回值 YES代表可以购买/NO代表没有权限购买。
载入目录 载入储存在本地或者远程的商品目录。
获得商品本地化信息 程序向App Store发起查询请求获得目录中商品的本地化信息:
myPossibleProds = [NSSet setWithObjects: @”myProd1”, @”myProd2”, @”myProd3, nil];
myProdRequest = [[SKProductsRequest alloc] initWithProductIdentifiers: myPossibleProds];
myProdRequest.delegate = myProductsRequestDelegate; [myProdRequest start];  
程序接收来自App Store的回应: -(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response response包含可能的信息 response.products //产品信息 response.invapdProductIdentifiers //错误的产品标示符 错误的产品可能由下面的原因导致:

商品未在 iTunes Connect中录入。
商品没有选择“Cleared for Sale”。
商品还没有传播到所有服务器。
创建商品信息 前面的步骤获得了产品信息的数组用以呈现:
myAvailableProductsArray = response.products;
for (SKProduct* aProduct in myAvailableProductsArray) {
    diplayTitle = aProduct.locapzedTitle;
    displayDesc = aProduct.locapzedDescription;
    displayPrice = aProduct.price;

呈现商品信息 在UI中显示前面获得的信息
请求支付
myPayment = [SKPayment paymentForProductIdentifier: selectedProdID userInfo: nil];  
myPayment = [SKPayment paymentForProduct: selectedProduct userInfo: nil]; 
然后提交到队列
defaultPaymentQueue = [SKPaymentQueue defaultQueue]; [defaultPaymentQueue addTransactionObserver: myObserver]; [defaultPaymentQueue addPayment: myPayment];  
[defaultPaymentQueue addPayment: myRequest];  
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions 
验证支付的事务
for(SKPaymentTransaction *aTransaction in transactions) {  
       switch (aTransaction.transactionState )  
           {
            // 商品进入购买流程
            case SKPaymentTransactionStatePurchasing: ... break;  
            // 成功购买商品  
            case SKPaymentTransactionStatePurchased: ... break;  
            // 已经购买过该商品  
            case SKPaymentTransactionStateRestored: ... break;  
            // 用户取消交易  
            case: SKPaymentTransactionStateFailed ... break;  
           }

Used to vapdate e-commerce transaction
Cryptographically signed chunk of data
Contains information about the purchase
Can vapdate using Web-based API
收到交易回执以后可以传送到你的服务器处理该交易。
你的服务器可以向苹果发送交易验证信息。 苹果的交易验证地址:https://buy.itunes.apple.com/verifyReceipt 验证数据格式:{ receipt-data : &helpp;} 苹果的返回值如果是{ status : 0 } 那么交易是正确,如果返回任何其他值 { status : any_other_value } 交易就是不成立。 你的服务器收到苹果的验证成功信息以后,返回给应用程序类似于解锁功能等消息。
解锁内置物品火提供远程存取地址 需要注意的地方

用户可能随时退出程序。
Store Kit 的交易队列是持久化的。
交易是持久化的。
验证存取。
移除交易。
[[SKPaymentQueue defaultQueue] finishTransaction: transaction]; 
储存购买 需要将非消耗的购买记录在服务器上,以供用户更换设备或删除程序以后重新安装时再次解锁功能。
提供物品恢复 前面的代码中,已经购买过该商品的处理就是直接解锁。 defaultPaymentQueue = [SKPaymentQueue defaultQueue]; [defaultPaymentQueue restoreCompletedTransactions];
测试沙盒环境
只供给测试用户,并且设备上才有效,不能使用模拟器测试。
可以为每个国家的商店添加测试帐号。
不能用来测试其他的iTunes Store特性。
需要为每个测试添加唯一的email地址,可以考虑使用+
payam+usa@example.com—测试美国的商品
payam+uk@example.com—测试英国的商品
payam+jp@example.com—测试日本的商品
测试步骤,需要在设备的设置菜单中iTunes Store登出原有帐户。
然后不要在设置菜单中登录测试帐号,而是直接打开需要测试的程序进行测试。
在这之前需要在iTunes Connect的Manage Users菜单中添加In App Purchase测试用户。
财务报表
收入和应用程序同样的规则
你获得总收入的70%。
支付日程,使用税,报表等全部都一样。
报表
一样的报表格式。
贩卖的标示符栏显示物品的产品ID。
所有者标示符栏显示程序的ID。

 
 
回复 支持 反对

举报

   
lareina

6#

  楼主| 发表于 2014-7-23 11:33:59 | 只看该作者

iOS应用内置付费 IAP设置总结

开始IAP开发并不需要多深的背景知识,这里囫囵看下去就可以了。但是要了解IAP的两种验证形式: 设备验证和服务器验证。在开始IAP开发前,先要对IAP有个大概的了解,下面这片文章就是给你预备的。

wjforstudy分享了IAP的一些基本知识。
from mobile.51cto.com
1.在开始IAP开发前,先要对IAP有个大概的了解,下面这片文章就是给你预备的:
苹果的官方文档:Store Kit Guide(In App Purchase)
现在网上已经有对这个文档很成熟的翻译了,如果想加快阅读速度,看这里
翻译版本:http://yarin.blog.51cto.com/1130898/549141
PS:开始IAP开发并不需要多深的背景知识,这里囫囵看下去就可以了。但是要了解IAP的两种验证形式: 设备验证和服务器验证。
2.网上流传比较广的IAP开发流程的帖子,讲的范围很全面,但是有的内容跟不上时代了,一共有两点需要注意:
一、在现在的IAP测试中,已经不需要提交程序再让开发者把程序状态设置“Developer Reject”了,只需要程序 到”prepare for upload“就可以了。
二、在IOS5,已经可以在模拟器中测试,调试IAP这个功能了。
这一篇文章地址是:
IAP(程序内购买): 完全攻略
http://fei263.blog.163.com/blog/static/927937242011321105241823/
这篇文章包含了从创建APP ID 开始到IAP基本功能实现的整个流程,很全面,值得花时间去看一看,但是注意几个问题:
1.文中有几个前置条件没有说明:
<1 在添加Test User之前,应该已经设置好ITunes Connect账户中关于Purchase Contract的信息。至少应该点击contract 的request按钮,让contract置于pending in process的状态。
完成上面的操作后,你才会在刚进入Manage User界面的时候,能选择创建Test User。
3.请保证这个时候你的pruchase contract已经完成了!contract中的 bank info,tax info,contact info都需要填写完成!
这篇帖子是这里要推荐的,主要介绍了Itunes Connect中创建了程序信息后,IAP的准备和测试的过程:
【iOS开发必收藏】详解iOS应用程序内使用IAP/StoreKit付费、沙盒(SandBox)测试、创建测试账号流程!
http://blog.csdn.net/xiaominghimi/article/details/6937097
4.遇到问题了?
好了,经过前面的准备后,就到了真正和IAP联通的步骤了。在输入一个Product ID向服务器发起request的时候,很有可能出现失败的情况,在request属性InvalidateIdentifier中,你会发现这个Product ID是无效的。
为什么呢?苹果是不会告诉你的,连个错误代码都没有,非常坑爹。
所以这里我们有一个Check List,需要大家逐条检查:
Have you enabled In-App Purchases for your App ID?
Have you checked Cleared for Sale for your product?
Have you submitted (and optionally rejected) your application binary?
Does your project’s .plist Bundle ID match your App ID?
Have you generated and installed a new provisioning profile for the new App ID?
Have you configured your project to code sign using this new provisioning profile?
Are you building for iPhone OS 3.0 or above?
Are you using the full product ID when when making an SKProductRequest?
Have you waited several hours since adding your product to iTunes Connect?
Are your bank details active on iTunes Connect? (via Mark)
Have you tried deleting the app from your device and reinstalling? (via Hector, S3B, Alex O, Joe, and Alberto)
Is your device jailbroken? If so, you need to revert the jailbreak for IAP to work. (via oh my god, Roman, and xfze)
If you answered “No” to any one of these questions, there’s your problem.
If you answered “Yes” for each of these questions and you still have an invalid product ID, then you have a problem I haven’t seen before. Check out the links in the next section, several of which are Developer Forum posts that were especially helpful in my hunt for debugging invalid product IDs.
5.再使用别人提供的例子代码测试IAP完成以后,这里向你介绍一个别人封装好的类库:ECPurchase
http://mobile.51cto.com/hot-410074.htm  类库在附件中下载。

 
 
回复 支持 反对

举报

   
lareina

7#

  楼主| 发表于 2014-7-23 11:35:10 | 只看该作者

iOS应用内置付费In-App Purchase 个人使用总结

添加Storekit.Framework,编写自己的storeObsever,用于处理交易,代码如下,其中completeTransaction 和failedTransaction两个函数是自定义的用来处理交易成功与失败其它的就都是SKPaymentTransactionObserver 这个代理要求的。

上一个项目用到了In App Purchase,发现现在大家对这个挺关注的,把上次写的总结贴出来给大家看一下,希望对大家有点帮助!
一、有关如何在程序中加入In APP Purchase 的内容参考下面的连接
http://developer.apple.com/iphone/library/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008267-CH1-SW1
二、我的程序具体步骤
1.添加Storekit.Framework,编写自己的storeObsever,用于处理交易,代码如下,其中completeTransaction 和failedTransaction两个函数是自定义的用来处理交易成功与失败其它的就都是SKPaymentTransactionObserver 这个代理要求的。
  • #import <Foundation/Foundation.h>
  • #import <StoreKit/StoreKit.h>
  • #import <StoreKit/SKPaymentTransaction.h>
  • @interface MyStoreObserver : NSObject < SKPaymentTransactionObserver > {
  • }
  • - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions;
  • -(void) PurchasedTransaction: (SKPaymentTransaction *)transaction;
  • - (void) completeTransaction: (SKPaymentTransaction *)transaction;
  • - (void) failedTransaction: (SKPaymentTransaction *)transaction;
  • -(void) paymentQueueRestoreCompletedTransactionsFinished: (SKPaymentTransaction *)transaction;
  • -(void) paymentQueue:(SKPaymentQueue *) paymentQueue restoreCompletedTransactionsFailedWithError:(NSError *)error;
  • @end
  • - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions函数用来更新transactions的状态
  • - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
  • for (SKPaymentTransaction* transaction in transactions)  {
  • switch (transaction.transactionState)  {
  • case SKPaymentTransactionStatePurchased:
  • [self completeTransaction:transaction];
  • break;
  • case SKPaymentTransactionStateFailed:
  • [self failedTransaction:transaction];
  • break;
  • case SKPaymentTransactionStateRestored:
  • break;
  • default:
  • break;
  • }
  • }
  • }
2.在程序中添加storeObsever,最好在applicationDidFinishLaunching中添加
  • MyStoreObserver *tempObserver = [[MyStoreObserver alloc] init];
  • self.observer = tempObserver;
  • [tempObserver release];
  • [[SKPaymentQueue defaultQueue] addTransactionObserver:self.observer];
3.发送付费请求,在相应的类中实现SKProductsRequestDelegate,别忘记定义自己产品的identifier
  • //请求产品信息
  • #define kMyFeatureIdentifier yourProductIdentifiers  (你自己的产品identifiers)
  • SKProductsRequest *request= [[SKProductsRequest alloc] initWithProductIdentifiers:
  • [NSSet setWithObject: kMyFeatureIdentifier]];
  • request.delegate = self;
  • [request start];
代理方法相关代码,如果请求成功的话就可以发送付费请求
  • #pragma mark request delegate
  • //!收到产品的消息
  • - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
  • SKPayment *payment = [SKPayment paymentWithProductIdentifier:kMyFeatureIdentifier];
  • [[SKPaymentQueue defaultQueue] addPayment:payment];
  • [request autorelease];
  • }
  • - (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
  • UIAlertView *alerView =  [[UIAlertView alloc] initWithTitle:@"Alert" message:[error localizedDescription]  delegate:nil c
  • ancelButtonTitle:NSLocalizedString(@"Close",nil)  otherButtonTitles:nil];  [alerView show]; [alerView release];
  • }
4.接下来就是Apple自己的付费相关处理了,需要做是就是点击按钮来确定付费
5.交易完成后需要向Apple验证这次交易是否成功,要不然没成功就把产品给别人的话,那就亏了,在1中可以看到当交易成功时会调用自定义的 completeTransaction函数,在该函数中我们需要验证transactionReceipt 关于验证SKPaymentTransaction的transactionReceipt transactionReceipt是只有当 SKPaymentTransaction完成时,即transactionState 被设置为SKPaymentTransactionStatePurchased 或 SKPaymentTransactionStateRestored时才被创建,因此只有这两种状态下能去验证transactionReceipt
原始思路及具体步骤如下:
a、从SKPaymentTranscation的实例中将transactionReceipt转化为NSString   
  • NSString *temptransactionReceipt  = [[NSString alloc] initWithData:[mytransaction transactionReceipt] encoding:NSUTF8StringEncoding];
如果用NSLog的方法将其写出来显示如下,里边有很多的‘+’
  • { "signature" =
  • "AZNZdoggtjbU/wMqZ4SSd3lgkxbWr+/zcV7Oez4io7f5oPMliKlQzWW4vj+FLsVyhjyyuPyTSugJ6m4Hrp+CjdAptGZg4iWExoyE6stltg0EfD8Ezggjg5q04ws74pMZ/0aRgjedua8dCMMqR7C8ZjojfOYU6LrFiK7qbUUiV+inMIIDUzCCAjugAwIBAgIIZRSRTdlYBLUwDQYJKoZIhvcNAQEFBQAwfzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFwcGxlIEluYy4xJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MTMwMQYDVQQDDCpBcHBsZSBpVHVuZXMgU3RvcmUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDkwNjE1MjIwNTU2WhcNMTQwNjE0MjIwNTU2WjBkMSMwIQYDVQQDDBpQdXJjaGFzZVJlY2VpcHRDZXJ0aWZpY2F0ZTEbMBkGA1UECwwSQXBwbGUgaVR1bmVzIFN0b3JlMRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAytGMXZy3gitJ2JMKFojSDynC/9yYezyn9HBX+u3/3VcpWE2XhcgGKYqNBA1+AewOzrKO774OsokTu4qymEx10ph8UTmsZewB0ESMHBEjF7FN6/HccsQUYC3WagrHnT12HG2Ih0OAm/ZhpWzj0HS4m813LpIyo00sewMvMNL2hkcCAwEAAaNyMHAwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBQ2HejinYLSARi1MmsO10MLkVhDOjAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFKmDg/IZSMU+ElcIFMzNo36ZXyT1MBAGCiqGSIb3Y2QGBQEEAgUAMA0GCSqGSIb3DQEBBQUAA4IBAQARpJs+O2Y3gL8gHdASkrfZHFpwINd1VcB5VF5LkVpnFz63zylA/3cGIDG91b/d5NIwZjkVt4Bgvd62o/mCbzCsWiNfSKTJVFK1D78BDQoSO2oHTuQuz1BR7xzNHxQZ90zUS6ZX9SC8N3g3A1jEtAyDhZNB+CRBBXLwZdnBUeBsT9QLpjvTnekZcGTnU08zfCjGF3eBJEu9eP6WgexK1xMSp72kEOmYbn6yTi3D4YrcYx4Q3n/57VBP2en8qXWeP5oHDsLTGzLRsWdoB3VxJLrF2ivL8JS8zqC0qyac452pN6xunRuzyyfpaqzQL12BzFEe44xna2byektSbtquA5LNAAAAAA=="; "purchase-info" = "ewoJIml0ZW0taWQiID0gIjMzMDU5OTg4MCI7Cgkib3JpZ2luYWwtdHJhbnNhY3Rpb24taWQiID0gIjEwMDAwMDAwMDAwOTEyNTgiOwoJInB1cmNoYXNlLWRhdGUiID0gIjIwMDktMTAtMTQgMDY6MDY6NTQgRXRjL0dNVCI7CgkicHJvZHVjdC1pZCIgPSAiY29tLnNlbnNreS5jbmFtZXNpZ3B1cmNoYXNlY29uc3VtYWJsZSI7CgkidHJhbnNhY3Rpb24taWQiID0gIjEwMDAwMDAwMDAwOTEyNTgiOwoJInF1YW50aXR5IiA9ICIxIjsKCSJvcmlnaW5hbC1wdXJjaGFzZS1kYXRlIiA9ICIyMDA5LTEwLTE0IDA2OjA2OjU0IEV0Yy9HTVQiOwoJImJpZCIgPSAiY29tLnNlbnNreS5jc2lnbmF0dXJlYXBwIjsKCSJidnJzIiA9ICIxLjAiOwp9"; "pod" = "100"; "signing-status" = "0";}   
b、然后通过Post的方法将其提交给服务器,在这里只是将transactionReceipt传给服务器,验证由服务器完成。代码如下,
  • NSString *requestStirng =[NSString stringWithFormat: @"receipt_data=%@",temptransactioReceipt];
  • requestStirng = [requestStirng stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
  • NSData *postData = [NSData dataWithBytes:[requestStirng UTF8String] length:[requestStirng length]];
  • NSMutableURLRequest *connectionRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[kURL          stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
  • [connectionRequest setHTTPMethod:@"POST"];
  • [connectionRequest setTimeoutInterval:120.0];
  • [connectionRequest setCachePolicy:NSURLRequestUseProtocolCachePolicy];
  • [connectionRequest setHTTPBody:postData];
                                                                    from mobile.51cto.com
出现的错误,服务器用传过去的receipt_data向Apple验证,通不过,但将用NSLog的方法在后台写出的 temptransactionreceipt向Apple验证能通过  错误原因  对比发现通过URL传给服务器的字符串不能很好的完成urlEncode,在里边出现的+不能自动进行编码转化,因为在Objective-C 中不能将NSString 真正的实现URL encode ,
解决方法可参考下面的地址  
http://simonwoodside.com/weblog/2009/4/22/how_to_really_url_encode/
但当我用上面地址给出的方法进行编码后仍不能解决该问题,原因不明
解决办法:
在将transactionReceipt转化成的NSString传给服务器之前先进行转化,将里边的+转换为%2B , 将步骤a中的代码改为
  • NSString *temptransactionReceipt  = [[NSString alloc] initWithData:[mytransaction transactionReceipt] encoding:NSUTF8StringEncoding];
  • temptransactionReceipt =  [temptransactionReceipt stringByReplacingOccurrencesOfString:@"+" withString:@"%2B"];
6.服务器端验证相关代码 其中XML为自定义的一个类,getHeader所做的工作就是把receipt数据Post给Apple然后得到返回的结果返回
  • $url = "https://buy.itunes.apple.com/verifyReceipt";/ $receipt = json_encode(array("receipt-data" => base64_encode($receipt_data)));
  • $response_json = $Xml->getHeader($url, $receipt);
  • $response = json_decode($response_json['content'], true);
  • getHeader的代码如下  public function getHeader($url, $data)         {
  • $ch = curl_init();
  • $timeout = 300; // set to zero for no timeout
  • curl_setopt($ch, CURLOPT_URL, $url);
  • //       curl_setopt($ch, CURLOPT_ENCODING, 'gzip');
  • curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 1); //post到https
  • curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  • curl_setopt($ch, CURLOPT_POST, true);
  • curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
  • curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  • curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);//跟随页面的跳转
  • //       curl_setopt($ch, CURLOPT_HEADER, true);
  • curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
  • $handles = curl_exec($ch);
  • $header = curl_getinfo($ch);
  • curl_close($ch);
  • $header['content'] = $handles;
  • return $header;
  • }
把自己写的测试的例子传上来供大家参考下
下载Demo例子源码下载地址:http://down.51cto.com/data/611901

 
 
回复 支持 反对

举报

   
lareina

8#

  楼主| 发表于 2014-7-23 11:36:17 | 只看该作者

iOS应用内置付费 ris'In App Purchase总结

虽然Apple的官方文档已经对In App Purhcase这一特性做了比较详尽的解释,但就某些细节方面还是需要编程人员进行尝试和推敲,今天我就为大家就In App Purchase做一剖析,以此抛砖引玉。

In App Purchase属于iPhone SDK3.0的新特性,用于在应用程序中购买付费道具,增加新功能,订阅杂志。是应用程序除了植入广告外的另一种取得收益的方式。
虽然Apple的官方文档已经对In App Purhcase这一特性做了比较详尽的解释,但就某些细节方面还是需要编程人员进行尝试和推敲,今天我就为大家就In App Purchase做一剖析,以此抛砖引玉。
In App Purchase的基本流程
1.登陆你的Apple开发者帐号(http://developer.apple.com/iphone)
2.创建一个新的Apple ID或是选用一个已存在的Apple ID,点击Config,启用In App Purchase功能。
3.创建develop(用于沙盒测试)和distribution(用于发布)的profile,创建时选择刚才创建的Apple ID。
4.编写你的应用程序(如何在应用程序中实现可以参考in app purchase的官方文档)
5.将你的应用提交到App Store.如果你的应用程序还没有完成只是需要测试,你可以在upload选项卡中选择upload your binary later,或者在提交后self reject你的应用,以免你的应用进入Apple的审核阶段。
6. 现在你可以为你的应用程序添加需要购买的东西了,在iTunes Connect中选择Manage Your In App Purchases,然后选择你的应用程序,开始添加你的购买物,Product ID是以后进行purchase操作的唯一识别,相当于主键,而且一旦添加后即使删除了以后也不允许再次使用这一ID(官方建议使用域名的命名模式 com.companyname.appname.productid)。Type共有三种选择:Non-Consumable(永久消费) Subscription(订阅) Consumable(可重复购买)。请勾选Cleared for Sale,如果不勾选,在测试时会返回invaild product id。填写好完整的商品信息后如果你的应用程序还未发布需要测试请选择submint with binary,否则请勾选submit now。
7.如果你需要测试你的purchase功能,upload你的应用程序,绑定商品到你的应用程序。
8.安装你的debug版本的应用程序到你的测试机器上进行测试。
                                                                  from mobile.51cto.com
In App Purchase的注意点
1.确保你所用来创建Profile的Apple ID启用了In App Purchase功能。
2.确保你的Apple ID的identifier中没有*。
3.确保你的bundle ID和你的Apple ID的identifier一致。
4.确保你的product ID是唯一的。
5.确保你在应用程序中所请求的product ID与你在iTunes Connect里添加的一致。
6.确保你勾选了Clear for Sale。
7.在测试的时候你可能需要等待你的商品添加入Apple的测试沙盒,这个过程可能需要几个小时。
8.在你第一次上传应用程序的时候,确保勾选了需要绑定至该应用程序的商品列表。
9.确保你是在SDK3.0以上编写的。
ECPurchase的使用
ECPurchase是我封装了purchase的内在逻辑,调用简单方便,如果你不想根据文档再自己写purchase功能,那么ECPurchase适合你。
1.在App Delegate中添加Observer
  • [[ECPurchase shared] addTransactionObserver];
2.设置ECPurchase的product delegate(产品列表代理),transaction delegate(购买结果代理),验证方式
  • [[ECPurchase shared] setProductDelegate:self];
  • [[ECPurchase shared] setTransactionDelegate:self];
  • [[ECPurchase shared] setVerifyRecepitMode:ECVerifyRecepitModeiPhone];
3.请求商品列表
  • [[ECPurchase shared] requestProductData:identifiers];
实现代理函数绘制UI
  • -(void)didReceivedProducts:(NSArray *)products;
4.购买商品
  • [[ECPurchase shared] addPayment:proIdentifier];
5.确认结果
如果不需要收据认证实现代理函数
  • -(void)didFailedTransaction:(NSString *)proIdentifier;
  • -(void)didRestoreTransaction:(NSString *)proIdentifier;
  • -(void)didCompleteTransaction:(NSString *)proIdentifier;
否则实现代理函数
  • -(void)didCompleteTransactionAndVerifySucceed:(NSString *)proIdentifier;
  • -(void)didCompleteTransactionAndVerifyFailed:(NSString *)proIdentifier withError:(NSString *)error;
源码下载地址: [url]http://down.51cto.com/data/637142[/url]

 
 
回复 支持 反对

举报

   
 
当用户需要购买某件产品时,程序调用StoreKit来收集购买信息。下图即为基本的store kit 模型:Store Kit的API只是为程序添加In App Purchase功能的一小部分。你需要决定如何去记录那些你想要提交的产品,如何在程序中将商店功能展现给用户,还要考虑如何将用户购买的产品提交。

一、In App Purchase概览
Store Kit代表App和App Store之间进行通信。程序将从App Store接收那些你想要提供的产品的信息,并将它们显示出来供用户购买。当用户需要购买某件产品时,程序调用StoreKit来收集购买信息。下图即为基本的store kit 模型:Store Kit的API只是为程序添加In App Purchase功能的一小部分。你需要决定如何去记录那些你想要提交的产品,如何在程序中将商店功能展现给用户,还要考虑如何将用户购买的产品提交。本章的剩余部分会展示整个流程。
Products
产品可以是任意一项你想要出售的特性。产品在iTunes Connect中被组织,这和你添加一个新的App是一样的。支持的产品种类共有四种:
1. 内容型。包括电子书,电子杂志,照片,插图,游戏关卡,游戏角色,和其他的数字内容。
2. 扩展功能。这些功能已经包含在App内部。在未购买之前被锁定。例如,你可以在一个游戏程序中包含若干个小游戏,用户可以分别来购买这些游戏。
3. 服务。允许程序对单次服务收费。比如录音服务。
4. 订阅。支持对内容或服务的扩展访问。例如,你的程序可以每周提供财务信息或游戏门户网站的信息。应该设定一个合理的更新周期,以避免过于频繁的提示困扰用户。要记住:你将负责跟踪订阅的过期信息,并且管理续费。App Store不会替你监视订阅的周期,也不提供自动收费的机制。
In App Purchase为创建产品提供了一种通用的机制,如何操作将由你负责。当你设计程序的时候,有以下几点需要注意:
1. 你必须提供电子类产品和服务。不要使用In App Purchase 去出售实物和实际服务。
2. 不能提供代表中介货币的物品,因为让用户知晓他们购买的商品和服务是很重要的。

 

from mobile.51cto.com
服务器类型
使用这终方式,要提供另外的服务器将产品发送给程序。 服务器交付适用于订阅、内容类商品和服务,因为商品可以作为数据发送,而不需改动程序束。 例如,一个游戏提供的新的内容(关卡等)。 Store Kit不会对服务器端的设计和交互做出定义,这方面工作需要你来完成。 而且,Store Kit不提供验证用户身份的机制,你需要来设计。 如果你的程序需要以上功能,例如,纪录特定用户的订阅计划, 你需要自己来设计和实现。
图1-3 展示了服务器类型的购买过程。

 

[/url]
1. 程序向服务器发送请求,获得一份产品列表。
2. 服务器返回包含产品标识符的列表。
3. 程序向App Store发送请求,得到产品的信息。
4. App Store返回产品信息。
5. 程序把返回的产品信息显示给用户(App的store界面)
6. 用户选择某个产品
7. 程序向App Store发送支付请求
8. App Store处理支付请求并返回交易完成信息。
9. 程序从信息中获得数据,并发送至服务器。
10. 服务器纪录数据,并进行审(我们的)查。
11. 服务器将数据发给App Store来验证该交易的有效性。
12. App Store对收到的数据进行解析,返回该数据和说明其是否有效的标识。
13. 服务器读取返回的数据,确定用户购买的内容。
14. 服务器将购买的内容传递给程序。
Apple建议在服务器端存储产品标识,而不要将其存储在plist中。 这样就可以在不升级程序的前提下添加新的产品。
在服务器模式下, 你的程序将获得交易(transaction)相关的信息,并将它发送给服务器。服务器可以验证收到的数据,并将其解码以确定需要交付的内容。 这个流程将在“验证store收据”一节讨论。
对于服务器模式,我们有安全性和可靠性方面的顾虑。 你应该测试整个环境来避免威胁。《Secure Coding Guide》文档中有相关的提示说明。
虽然非消耗性商品可以用内置模式来恢复,订阅类商品必须通过服务器来恢复。你要负责纪录订阅信息、恢复数据。
消耗类商品也可以通过服务器方式来纪录。例如,由服务器提供的一项服务, 你可能需要用户在多个设备上重新获得结果。
(这段翻译的比较生硬,因为我个人也没有机会把各种类型的服务跑一遍,后续会检查并修改。希望大家一起来看看,欢迎补充。)
取得产品信息
要在程序内部显示“商店”,需要从App Store得到信息来购建界面。 本章详细讲解如何从App Store获取产品信息。
向App Store发送请求
Store Kit提供了从App Store上请求数据的通用机制。 程序可以创建并初始化一个request对象, 为其附加delegate, 然后启动请求过程。请求将被发送到App Store,在那里被处理。 处理完成时, request对象的delegate方法将被异步调用,以获得请求的结果。 图2-1显示了请求的数据模型。

 

如果程序在请求期间退出,则需要重新发送请求。
下面讲解请求过程中用到的类:
SKRequest
SKRequest为request的抽象根类。
SKRequestDelegate
SKRequestDelegate是一个protocol, 实现用以处理请求结果的方法,比如请求成功,或请求失败。
发送获得产品信息的请求
程序使用products request来获得产品的信息。 要完成这一过程,程序需创建一个request对象,其中会包含一个产品标识的列表。之前提到过,你的程序既可以内置产品列表,又可以通过外部服务器来获得。
当发送请求时,产品标识会传送到App Store,App Store将会返回本地化信息(这些信息事先已经在iTunes Connect中设置好了),你将使用这些信息来购建内置商店的界面(显示商品名,描述,等等)。 图2-2显示了请求的过程。

 

SKProductsRequest
用来请求商品的信息。 创建时,我们将需要显示的商品列表加入该对象。
SKProductsRequestDelegate
该protocol定义了处理App Store响应的方法。
SKProductsResponse
SKProductsResponse对象为App Store返回的响应信息。里面包含两个列表(当然是NSArray了):一是经过验证有效的商品,
@property(nonatomic, readonly) NSArray *products
另外一个是无法被识别的商品信息:
@property(nonatomic, readonly) NSArray * invalidProductIdentifiers
有几种原因将造成商品标识无法被识别,如拼写错误(当然),被标记为不可出售(unavailable for sale),或是对商品信息的改变没有传送到所有App Store的服务器。(这个原因不是很清楚,再议)。
SKProduct
SKProduct对象包含了在App Store上注册的商品的本地化信息。
购买商品
当用户准备购买商品时,程序向App Store请求支付信息,然后App Store将会创建持久化的交易信息,并继续处理支付流程,即使用户重启程序,这个过程亦是如此。App Store同步待定交易的列表到程序中,并在交易状态发生改变时向程序发送更新的数据。
收集支付信息
要收集支付信息, 你的程序可以创建一个payment的对象,将它放到支付队列中,如图3-1所示。

 

1. 一个SKPayment的对象,包含了"Sword"的商品标识,并且制定购买数量为1。
2. 使用addPayment:方法将SKPayment的对象添加到SKPaymentQueue里。
3. SKPaymentmentQueue包含的所有请求商品,
4. 使用SKPaymentTransactionObserver的paymentQueue: updatedTransactions: 方法来检测所有完成的购买,并发送购买的商品。
5. 最后,使用finishTransaction:方法完成交易。
当payment的对象被添加到支付队列中的时候, 会创建一个持久保存的transaction对象来存放它。 当支付被处理后,transaction被更新。 程序中将实现一个观察者(observer)对象来获取transaction更新的消息。 观察者应该为用户提供购买的商品,然后将transaction从队列中移除。
下面介绍在购买过程中用到的几个类:
SKPayment
要收集支付信息,先要了解一下支付对象。 支付对象包含了商品的标识(identifier)和要购买商品的数量(quantity)(数量可选)。你可以把同一个支付对象重复放入支付队列,,每一次这样的动作都相当于一次独立的支付请求。
用户可以在Settings程序中禁用购买的功能。 因此在请求支付之前,程序应该首先检查支付是否可以被处理。 调用SKPaymentQueue的canMakePayments方法来检查。
SKPaymentQueue
支付队列用以和App Store之间进行通信。 当新的支付对象被添加到队列中的时候, Store Kit向App Store发送请求。 Store Kit将会弹出对话框询问用户是否确定购买。 完成的交易将会返回给程序的observer对象。
SKPaymentTransaction
transaction对象在每次添加新的payment到队列中的时候被创建。 transaction对象包含了一些属性,可以让程序确定当前的交易状态。
程序可以从支付队列那里得到一份审核中的交易列表,但更常用的做法还是等待支付队列告知交易状态的更新。
SKPaymentTransactionObserver
在程序中实现SKPaymentTransactionObserver的协议,然后把它作为SKPaymentQueue对象的观察者。该观察者的主要职责是:检查完成的交易,交付购买的内容,和把完成后的交易对象从队列中移除。
在程序一启动,就应该为支付队列指定对应的观察者对象,而不是等到用户想要购买商品的时候。 Transaction对象在程序退出时不会丢失。程序重启时, Store Kit继续执行未完成的交易。 在程序初始化的时候添加观察者对象,可以保证所有的交易都被程序接收(也就时说,如果有未完成的transaction,如果程序重启,就重新开始了,如 果稍候再添加观察者,就可能会漏掉部分交易的信息)。
恢复交易信息(Transactions)
当transaction被处理并从队列移除之后,正常情况下,程序就再也看不到它们了。 如果你的程序提供的是非消耗性的或是订阅类的商品,就必须提供restore的功能,使用户可以在其他设备上重新存储购买信息。
Store Kit提供内建的功能来重新存储非消耗商品的交易信息。 调用SKPaymentQueue的restoreCompletedTransactions的方法来重新存储。对于那些之前已经完成交易的非消耗性商 品,Apple Store生成新的,用于恢复的交易信息。 它包含了原始的交易信息。你的程序可以拿到这个信息,然后继续为购买的功能解锁。 当之前所有的交易都被恢复时, 就会调用观察者对象的paymentQueueRestoreCompletedTransactionsFinished方法。
如果用户试图购买已经买过的非消耗性商品,程序会收到一个常规的交易信息,而不是恢复的交易信息。但是用户不会被再次收费。程序 应把这类交易和原始的交易同等对待。
订阅类服务和消耗类商品不会被Store Kit自动恢复。 要恢复这些商品,你必须在用户购买这些商品时,在你自己的服务器上记录这些交易信息, 并且为用户的设备提供恢复交易信息的机制。
在程序中添加Store功能
本章为添加购买功能的指导
详细流程:
准备工作当然是添加StoreKit.framework了。
然后是具体的步骤:
1. 决定在程序内出售的商品的类型。
之前提到过,程序内可以出售的新feature类型是有限制的。 Store Kit不允许我们下载新的代码。 你的商品要么可以通过当前的代码工作(bundle类型),要么可以通过服务器下载(当然,这里下载的为数据文件,代码是不可以的)。 如果要修改源代码,就只能老实的升级了。
2. 通过iTunes Connect注册商品
每次添加新商品的时候都需要执行这一步骤。 每个商品都需要一个唯一的商品标识。 App Store通过这个标识来查找商品信息并处理支付流程。 注册商品标识的方法和注册程序的方法类似。
要了解如何创建和注册商品信息,请参考“iTunes Connect Developer Guide”文档。
3. 检测是否可以进行支付
用户可以禁用在程序内部支付的功能。在发送支付请求之前,程序应该检查该功能是否被开启。程序可在显示商店界面之前就检查该设置(没启用就不显示商店界面了),也可以在用户发送支付请求前再检查,这样用户就可以看到可购买的商品列表了。
例子:
if([SKPaymentQueue canMakePayments])   
{   
    ...//Display a store to the user   
}   
else  
{   
    ...//Warn the user that purchases are disabled.   
}  
4. 获得商品的信息
程序创建SKProductsRequest对象,用想要出售的商品的标识来初始化, 然后附加上对应的委托对象。 该请求的响应包含了可用商品的本地化信息。
//这里发送请求   
- (void)requestProductData   
{   
    SKProductsRequest *request = [[SKProductsRequest alloc]initWithProductIdentifiers:   
    [NSSet setWithObject: kMyFeatureIdentifier]];   
      
    request.delegate = self;   
    [request start];   
}   
  
//这个是响应的delegate方法   
- (void)productsRequest: (SKProductsRequest *)request   
didReceiveResponse: (SKProductsResponse *)response   
{   
    NSArray *myProduct = response.products;   
  
    //生成商店的UI   
    [request autorelease];   
}  
5. 添加一个展示商品的界面
Store Kit不提供界面的类。 这个界面需要我们自己来设计并实现。
6. 为支付队列(payment queue)注册一个观察者对象
你的程序需要初始化一个transaction observer对象并把它指定为payment queue的观察者。
上代码:
MyStoreObserver *observer = [[MyStoreObserver alloc]init];   
[[SKPaymentQueue defaultQueue]addTransactionObserver: observer];  
应该在程序启动的时候就添加好观察者,原因前面说过,重启后程序会继续上次未完的交易,这时就添加观察者对象就不会漏掉之前的交易信息。
7. 在MyStoreObserver类中执行paymentQueue: updatedTransactions: 方法。
这个方法会在有新的交易被创建,或者交易被更新的时候被调用。
- (void)paymentQueue: (SKPaymentQueue *)queue updatedTransactions: (NSArray *)transactions   
{   
    for(SKPaymentTransaction * transaction in transactions)   
    {   
        switch(transaction.transactionState)   
        {   
            case SKPaymentTransactionStatePurchased:   
                [self completeTransaction: transaction];   
                break;   
            case SKPaymentTransactionStateFailed:   
                [self failedTransaction: transaction];   
                break;   
            case SKPaymentTransactionStateRestored:   
                [self restoreTransaction: transaction];   
            default:   
                break;   
        }   
    }   
}  
上面的函数针对不同的交易返回状态,调用对应的处理函数。
8. 观察者对象在用户成功购买一件商品时,提供相应的内容,以下是在交易成功后调用的方法
- (void) completeTransaction: (SKPaymentTransaction *)transaction   
{   
    //你的程序需要实现这两个方法   
    [self recordTransaction: transaction];   
    [self provideContent: transaction.payment.productIdentifier];   
      
    //将完成后的交易信息移出队列   
    [[SKPaymentQueue defaultQueue]finishTransaction: transaction];   
}  
交易成功的信息包含transactionIdentifier和transactionReceipt的属性。其中,transactionReceipt记录了支付的详细信息,这个信息可以帮助你跟踪、审(我们的)查交易,如果你的程序是用服务器来交付内 容,transactionReceipt可以被传送到服务器,然后通过App Store验证交易。(之前提到的server模式,可以参考以前的图)
9. 如果交易是恢复过来的(restore),我们用这个方法来处理:
- (void) restoreTransaction: (SKPaymentTransaction *)transaction   
{   
    [self recordTransaction: transaction];   
    [self provideContent: transaction.payment.productIdentifier];   
  
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];   
}  
这个过程完成购买的过程类似。 恢复的购买内容提供一个新的交易信息,这个信息包含了新的transaction的标识和receipt数据。 如果需要的话,你可以把这些信息单独保存下来,供追溯审(我们的)查之用。但更多的情况下,在交易完成时,你可能需要覆盖原始的transaction数 据,并使用其中的商品标识。
10. 交易过程失败的话,我们调用如下的方法:
- (void)failedTransaction: (SKPaymentTransaction *)transaction   
{   
    if(transaction.error.code != SKErrorPaymentCancelled)   
    {   
        //在这类显示除用户取消之外的错误信息   
    }   
  
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];   
}  
通常情况下,交易失败的原因是取消购买商品的流程。 程序可以从error中读出交易失败的详细信息。
显示错误信息不是必须的,但在上面的处理方法中,需要将失败的交易从支付队列中移除。 一般来说,我们用一个对话框来显示错误信息,这时就应避免将用户取消购买这个error显示出来。
11. 组织好程序内“商店”的UI。当用户选择一件商品时, 创建一个支付对象,并放到队列中。
SKPayment *payment = [SKPayment paymentWithProductIdentifier: kMyFeatureIdentifier];   
[[SKPaymentQueue defaultQueue] addPayment: payment];  
如果你的商店支持选择同一件商品的数量,你可以设置支付对象的quantity属性
SKMutablePayment *payment = [SKMutablePayment paymentWithProductIdentifier: kMyFeatureIdentifier];   
payment.quantity = 3;   
[[SKPaymentQueue defaultQueue] addPayment: payment];  
下一步:
本章中所示代码可用于内置型商品模式(Built-in)。 如果你的程序要使用服务器来发布商品,你需要负责设计和执行iPhone程序和你的服务器之间的通信。服务器应该验证数据并为程序提供内容。
验证store的收据
使用服务器来交付内容,我们还需要做些额外的工作来验证从Store Kit发送的收据信息。
重要信息:来自Store的收据信息的格式是专用的。 你的程序不应直接解析这类数据。可使用如下的机制来取出其中的信息。
验证App Store返回的收据信息
当交易完成时,Store Kit告知payment observer这个消息,并返回完成的transaction。 SKPaymentTransaction的transactionReceipt属性就包含了一个经过签名的收据信息,其中记录了交易的关键信息。你的 服务器要负责提交收据信息来确定其有效性,并保证它未经过篡改。 这个过程中,信息被以JSON数据格式发送给App Store,App Store也以JSON的格式返回数据。(大家可以先了解一下JSON的格式)
验证收据的过程:
1. 从transaction的transactionReceipt属性中得到收据的数据,并以base64方式编码。
2. 创建JSON对象,字典格式,单键值对,键名为"receipt-data", 值为上一步编码后的数据。效果为:
{   
    "receipt-data"    : "(编码后的数据)"  
}  
3. 发送HTTP POST的请求,将数据发送到App Store,其地址为:
[url=https://buy.itunes.apple.com/verfyReceipt]https://buy.itunes.apple.com/verfyReceipt
4. App Store的返回值也是一个JSON格式的对象,包含两个键值对, status和receipt:
{   
    "status"    : 0,   
    "receipt"    : { … }   
}  
如果status的值为0, 就说明该receipt为有效的。 否则就是无效的。
App Store的数据
发送给App Store的收据数据是通过对transaction中对应的信息编码而创建的。 当App Store验证收据时, 将从其中解码出数据,并以"receipt"的键返回。 返回的响应信息是JSON格式,被包含在SKPaymentTransaction的对象中(transactionReceipt属性)。Server 可通过这些值来了解交易的详细信息。 Apple建议只发送receipt数据到服务器并使用receipt数据验证和获得交易详情。 因为App Store可验证收据信息,返回信息,保证信息不被篡改,这种方式比同时提交receipt和transaction的数据要安全。(这段得再看看)
表5-1为交易信息的所有键,很多的键都对应SKPaymentTransaction的属性。
备注:一些键取决于你的程序是链接到App Store还是测试用的Sandbox环境。更多关于sandbox的信息,请查看"Testing a Store"一章。
Table 5-1 购买信息的键:

 

测试Store功能
开发过程中,我们需要测试支付功能以保证其工作正常。然而,我们不希望在测试时对用户收费。 Apple提供了sandbox的环境供我们测试。
备注:Store Kit在模拟器上无法运行。 当在模拟器上运行Store Kit的时候,访问payment queue的动作会打出一条警告的log。测试store功能必须在真机上进行。
Sandbox环境
使用Sandbox环境的话,Store Kit并没有链接到真实的App Store,而是链接到专门的Sandbox环境。 SandBox的内容和App Store一致,只是它不执行真实的支付动作。 它会返回交易成功的信息。 Sandbox使用专门的iTunes Connect测试 账户。不能使用正式的iTunes Connect账户来测试。
要测试程序,需要创建一个专门的测试账户。你至少需要为程序的每个区域创建至少一个测试账户。详细信息,请查看iTunes Connect Developer Guide文档。
在Sandbox环境中测试
步骤:
1. 在测试的iPhone上退出iTunes账户
Settings中可能会记录之前登录的账户,进入并退出。
重要信息:不能在Settings 程序中通过测试账户登录。
2. 运行程序
当你在程序的store中购买商品后,Store kit提示你去验证交易。用测试账户登录,并批准支付。 这样虚拟的交易就完成了。
在Sandbox中验证收据
验证的URL不同了:
NSURL *sandboxStoreURL = [[NSURL alloc]initWithString:   
@"https://sandbox.itunes.apple.com/verifyReceipt"];

iOS应用内置付费详尽攻略相关推荐

  1. IAP(程序内购买): 完全攻略

    第一印象觉得In-App Purchase(简称IAP)非常简单.Apple提供的大量文档应该让开发者很快熟悉地熟悉.那么,为什么在你的应用中集成IAP特性就如此令人生厌呢?  这是因为在开发过程中不 ...

  2. 文明重启怎么找回之前的服务器,文明重启房子消失怎么找回 详尽攻略助你一臂之力...

    屋子怎么看不到了?文明重启房子消失怎么找回?还不知道的朋友看过来,这儿小编为各位提供王牌战争文明重启关于房子的攻略详解!感兴趣的朋友不容错失喔! 屋子消散解决之道攻略 然而并非呆在屋子中就十拿九稳了, ...

  3. 内置付费 inapp purchase and Error Domain=SKErrorDomain Code=0 “Cannot connect to iTunes Store”错误...

    MKStoreKit 3.0+ 内置付费的开源库 Mugunth Kumar大神的 英文iPhone Tutorial – In-App Purchases 中文翻译的地址 http://www.ov ...

  4. c 中html上传文件大小,IOS微信内置浏览器对html标签input type=file上传的文件大小size错误?...

    完整代码: function _s(){ var f = document.getElementById("f").files; //上次修改时间 alert(f[0].lastM ...

  5. js禁止苹果页面底部滚动_Vue.js 实现禁止 h5 页面在 ios 浏览器内置的下拉 bounce 效果...

    介绍 vue-disbounce是一款基于Vue.js的自定义组件,可以有效避免触发h5页面在ios浏览器内置的下拉bounce效果. 组件 :style="{'background-col ...

  6. 华南理工计算机考研王道论坛,复习详尽攻略:梦圆华工中探花

    复习详尽攻略:梦圆华工中探花 自助者天助之,考研也是如此.你必须十分努力,才能看起来毫不费力.下面分享一位前辈的考研经验. 作者 91淘气小卒 次阅读 2017-01-16 [摘要]自助者天助之,考研 ...

  7. iOS 用内置浏览器Safari 打开网页

    iOS 开发的时候,我们需要打开某个网页,可以写一个web页面,也可直接使用浏览器打开网址 那么我们怎么样使用iOS 内置的浏览器打开网址呢? 如下: ios 10 之前使用 [[UIApplicat ...

  8. iOS 用内置浏览器Safari 打开网页

    iOS 开发的时候,我们需要打开某个网页,可以写一个web页面,也可直接使用浏览器打开网址 那么我们怎么样使用iOS 内置的浏览器打开网址呢? 如下: ios 10 之前使用 [[UIApplicat ...

  9. 壁纸 | iOS 14 内置这几张壁纸,你喜欢吗?

    今日分享一点壁纸,这些壁纸是iOS14内置的,不用更新iOS14也能使用上iOS14的壁纸!到位!原图在最下方小程序中下载哦! 图片来源:网络,如侵联删 今日壁纸原图 识别下方小程序码即可下载原图哦! ...

最新文章

  1. 什么是API,SDK?它们之间有什么关系?
  2. 读书不言迟,不读终身痴[转]
  3. 现在好用的mysql客户端_还在用 Navicat 的,可以试试这几款免费且好用的 MySQL 客户端...
  4. Vue.js 2 渐进式前端框架 的最佳学习方法
  5. Git学习笔记:修改
  6. 卧槽!12个杭州阿里高学历女员工被初中男骗财骗色1900多万!骗子冒充有钱佬!开豪车保时捷勾搭妹子,法院判无期徒刑!...
  7. 通过远程(vnc)无法打开qtcreator
  8. 推荐Android学习-农民伯伯
  9. linux版本与内核对应关系,[科普] Linux 的内核与 Linux 系统之间的关系
  10. python进阶17炫技巧
  11. Java 1.3.1 带标签的 break语句
  12. java中多态_java之多态
  13. 栈、堆、静态存储区的三分天下
  14. 10分钟教你从零开始python_10分钟教你从零开始学python入门
  15. linux 在固定网址yum,linux yum介绍
  16. 汇编语言之div命令
  17. 遗传算法GA原理及实现(python实现GA求解TSP代码)
  18. NRF24L01-状态寄存器
  19. 选择合适的 Go 字符串拼接方式
  20. 想剑网三妹子最多服务器,每当谈论起哪款端游女生玩家多,为什么我们总是会想起剑网3?...

热门文章

  1. 详解 ▏跨境电商9710出口报关-跨境知道
  2. 游戏加速器的流程和具体延迟是什么?
  3. APT攻击是什么?该如何预防?
  4. Vue基础+Vue3新特性
  5. matlab模拟股票价格,MATLAB进行模拟股票价格???
  6. 前端——CSS如何使文字居中
  7. 什么是站群服务器?站群服务器性能如何?
  8. chrome 通知权限管理_如何在Chrome浏览器中阻止或管理通知
  9. 不止六周陪产假!微软向所有全职员工提供三个月的带薪育儿假
  10. Linux/Android——Input系统之frameworks层InputManagerService (六)【转】