[-]
- In App Rage
- 管理 In App Purchases
- Retrieving Product List提取产品列表
- Subclassing for Your App
- 添加帮助类代码
- 显示产品列表
- 给我钱看看
- In App Purchases Accounts and the Sandbox
- 何去何从
前言:自我总结---
loadStore(监听交易,请求产品描述:完成交由委托->获取产品描述-.交给fetch自定义委托LB显示)->
2,点击购买->激活交易监听进行购买payment->交易监听事件(相当于委托)-主要与store交互实现购买->
3,显示购买uialertview购买界面->cancel-正常消失;
购买:输入账号view--输入正确时,激发交易监听事件(主要与store交互实现购买)
一,IAP可以正常更年工作的主要2个步骤:
1,创建及提取产品描述;2,购买产品。
步骤1详细:创建唯一的APP ID->生成及安装新的provisioning profile文件->在Xcode中更新bundle ID及code signing profile->如果还没做的话,请在iTunes Connect中提交你程序相关的metadata和二进制码->为IAP添加新产品->编写提取产品描述的代码.
注:等待几小时,进行后续操作;提取产品描述的代码非常简单,但其他步骤则很容易错;为提取产品描述,并不需要在iTunes Connect中创建IAP测试用户。
详细:
1. 创建唯一的App ID
为支持IAP,你的App ID不能包括通配符(“*”)。为确定你的App Id是否包括通配符,请登录http://developer.apple.com/iphone,在 iPhone Developer Program Portal中选择左边菜单中的 “App IDs”检查你的 App ID。
下面是一个唯一的App ID:
7DW89RZKLY.com.runmonster.runmonsterfree
下面不是一个唯一的 App ID:
7DW89RZKLY.com.runmonster.*
如果你还没有一个唯一的App ID,按如下步骤创建一个:
在developer portal中的 App IDs 部分,选择“New App ID”
填写下列信息:
Display name(显示名): 选取一个不同的App ID的名称。你不能编辑或删除旧的App ID,所以你必须为你的App ID提供一个新名称以避免溷淆。
Prefix(前缀): 生成一个新的前缀,或者如果你的程序是通过Keychain Services API分享数据的系列程序中之一的话,则选用已存在的前缀。
Suffix(后缀): com.companyname.appname (这是通用格式 – 注意没有使用通配符)。
按 “Save”
按 App ID旁的“Configure” 链接
选取 “Enable In App Purchase”选择框
按“Done”
2. 创建一个新的Provisioning Profile文件
在创建了新的App ID后,你需要生成一个指向这个App ID的新provisioning profile。
下面就是令人痛苦的生成和安装新provisioning profile的详细步骤:
在 iPhone Developer Portal中, 选择左边的Provisioning部分
确保你处于Development 标籤下, 按下右上角的 “New Profile”
填入所需信息并指向你刚创建的唯一的App ID
如果你在Actions条目下看到 “Pending”,那麽请按下“Development”标籤标题进行刷新
点击 “Download” 下载新的profile文件
将profile文件拖入到Dock中Xcode图标上进行安装
如果你想在硬盘上保存provisioning profile,那麽你可以按如下步骤手工安装profile:
在Xcode中, 选择 Window > Organizer
选择左边 “Provisioning Profiles” 分类
Ctrl-按下profile > Reveal in Finder
将新profile拖入到 profile Finder 窗口
3. 更新Xcode 设置
在Xcode中安装了 profile 文件后,你需要对使用此provisiong profile的项目进行一些编辑工作:
编辑项目 .plist 文件使其 Bundle ID 与 App ID 匹配。忽略ID开始部分的字母数字序列。例如,在Developer Portal中你的App ID为“7DW89RZKLY.com.runmonster.runmonsterfree”,那麽在Bundle ID中你只需输入“com.runmonster.runmonsterfree” 。
编辑项目的 target 信息以使用新的provisioning profile:
选取 Project > Edit Active Target
选取顶部“Build” 标籤
选取需要的 configuration (通常为 Debug)
在Code Signing Identity中选择新的provisioning profile
在Code Signing Identity之下的行中(可能名为 Any iPhone OS Device)选择新的provisioning profile
4. 添加你的应用程序
如果你的程序已经发表到App Store了,那麽可以略过此步骤。
在你将产品添加到 iTunes Connect之前,你必须添加此产品所需的程序。如果你的程序还没有100%完成也无需担心,你可以先提交具有部分数据的程序,最后再提交真实的程序。
注意: 只有 SKU 和 version(版本)部分是以后不可修改的
登录到 http://developer.apple.com/iphone 点击右边链接进入 iTunes Connect
注意:你必须先登录到developer.apple.com,否则会有不测发生(译者注:具体是什麽不测我也不太清楚,胆大的请自己试一下)
在 iTunes Connect主页点击 “Manage Your Applications”
在右上角点击“Create New Application”
填写程序所需的一切信息。当要求程序二进制码时,请选择稍后上传选项。
5. 提交程序二进制码
Apple的文档中没有任何地方提及详情,但它却是必须的步骤。要成功测IAP功能,你必须提交程序的二进制码。即使你的程序还没有100%完成,你仍然需要提交二进制码。然而,你也可以立即摈弃你的二进制码,使其不会进入审核阶段。
下面这些步骤非常关键,我可是因为少做了某些步骤而度过了一段非常痛苦的时间:
生成App Store发佈版程序
如果你不知怎麽做,请在 iPhone Developer Portal 中点击左方的 Distribution标籤,并选择 “Prepare App” 标籤。然后,根据蓝色链接的指示:
获取iPhone发行许可证
创建并下载在App Store发行所需的iPhone Distribution Provisioning Profile
在Xcode中生成程序的发行版
在iTunes Connect中进入程序页
选择 “Upload Binary”
上传.zip压缩程序
如果你的程序还没有100%完成以进行审核,那麽请点击iTunes Connect中你程序首页中的 “Reject Binary”链接。程序的状态应该更新为 “Developer Rejected”.
不用担心,由于程序的状态是“Developer Rejected”,Apple是不会对其进行审核的。你可以在任何时候提交程序的新版本并使其状态为“Developer Rejected”,这不会对以后程序正式提交的等待时间有任何影响。
6. 添加产品
完成了以上所有步骤后,我们最终可以向iTunes Connect中添加产品了。
确保登录到 http://developer.apple.com/iphone 进入 iTunes Connect 主页
点击“Manage Your Applications”
点击刚建好的程序 点击view details
点击 “Manage in-App Purchases” 链接
点击 “Create New”
填写下列产品信息:
Reference Name(参考名称): 产品的通用名称。比如,我使用的是 “Pro Upgrade”。此名称是不允许进行编辑的,它不会显示于App Store中。
Product ID(产品ID): 你产品的唯一id。通常格式是 com.company.appname.product,但它可以说任何形式。它并不要求以程序的App ID作为前缀。
Type(类型): 有三种选择
Non-consumable(非消耗品): 仅需付费一次 (例如你希望将出现从免费版升级为专业版)
Consumable(消耗品): 每次下载都需要付费
Subscription(预订): 循环反覆
Price Tier(价格等级): 产品价格。参见不同等级的价格列表。
Cleared for Sale(等待销售): 一定要选取此项,否则的话,测试时会发生非法产品ID的错误。
Language to Add(增加的语言): 选一项。下列两项将出现:
Displayed Name(显示名称): 用户看到的产品名称。比如我选择 “Upgrade to Pro”。
Description(描述): 对产品进行描述。此处输入的文本将与Displayed Name 及 Price 一起在你代码中提取 SKProduct时出现。
Screenshot(截屏): 展示你产品的截屏。儘管屏幕上会显示“提交截屏会触发产品审核过程”之类的文字(个人拙见,这是非常糟糕的设计),你还是可以安全地提交截屏而不会使产品进入审核过程。存储后,选择“Submit with app binary” (随程序二进制码一起提交)选项。是产品与程序二进制绑定在一起,所以在你最后正式提交100%完成的程序二进制码时,产品也会随之提交。
点击 “Save”
最后一定不要忘了回到view details 编辑In-App Purchases
选择刚刚添加的iap版本。
Products
产品可以是任意一项你想要出售的特性。产品在iTunes Connect中被组织,这和你添加一个新的App是一样的。支持的产品种类共有四种:
1. 内容型。包括电子书,电子杂志,照片,插图,游戏关卡,游戏角色,和其他的数字内容。
2. 扩展功能。这些功能已经包含在App内部。在未购买之前被锁定。例如,你可以在一个游戏程序中包含若干个小游戏,用户可以分别来购买这些游戏。
3. 服务。允许程序对单次服务收费。比如录音服务。
4. 订阅。支持对内容或服务的扩展访问。例如,你的程序可以每周提供财务信息或游戏门户网站的信息。应该设定一个合理的更新周期,以避免过于频繁的
提示困扰用户。要记住:你将负责跟踪订阅的过期信息,并且管理续费。App Store不会替你监视订阅的周期,也不提供自动收费的机制。
7. 编写代码
下面我们开始编写代码对刚加入到iTunes Connect中的产品信息进行提取。我访问产品数据,我们需要使用 StoreKit framework。
注意: StoreKit 无法在模拟器上工作。你必须在真机上进行测试。
1.添加 StoreKit framework 到你的项目中。
2.添加SKProduct引用到你的 .h 文件中:
1. // InAppPurchaseManager.h
2.
3. #import <StoreKit/StoreKit.h>
4.
5. #define kInAppPurchaseManagerProductsFetchedNotification @"kInAppPurchaseManagerProductsFetchedNotification"
6.
7. @interface InAppPurchaseManager : NSObject <SKProductsRequestDelegate>
8. {
9. SKProduct *proUpgradeProduct;
10. SKProductsRequest *productsRequest;
11. }
注意: InAppPurchaseManager 是一个单例类,它处理程序中所有IAP任务。它是本文中的示例程序。
3.产品请求,并在相应.m文件中实现代理协议
:
• // InAppPurchaseManager.m
•
• - (void)requestProUpgradeProductData
• {
• NSSet *productIdentifiers = [NSSet setWithObject:@"com.runmonster.runmonsterfree.upgradetopro" ];
• productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
• productsRequest.delegate = self;
• [productsRequest start];
•
• // we will release the request object in the delegate callback
• }
•
• #pragma mark -
• #pragma mark SKProductsRequestDelegate methods
•
• - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
• {
• NSArray *products = response.products;
• proUpgradeProduct = [products count] == 1 ? [[products firstObject] retain] : nil;
• if (proUpgradeProduct)
• {
• NSLog(@"Product title: %@" , proUpgradeProduct.localizedTitle);
• NSLog(@"Product description: %@" , proUpgradeProduct.localizedDescription);
• NSLog(@"Product price: %@" , proUpgradeProduct.price);
• NSLog(@"Product id: %@" , proUpgradeProduct.productIdentifier);
• }
•
• for (NSString *invalidProductId in response.invalidProductIdentifiers)
• {
• NSLog(@"Invalid product id: %@" , invalidProductId);
• }
•
• // finally release the reqest we alloc/init’ed in requestProUpgradeProductData
• [productsRequest release];
•
• [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerProductsFetchedNotification object:self userInfo:nil];
• }
• 上面代码有几点需要注意:
指定产品id时,你必须使用完整产品id。例如,上例中使用 “com.runmonster.runmonsterfree.upgradetopro”。仅使用 “upgradetopro” 将不会正常工作。
如果在productsRequest:didReceiveResponse:中response.products 为 nil,而你的产品id出现于 response.invalidProductIdentifers 数组中时,那麽请做好心理准备开始一场徒劳的搜索战吧。 StoreKit API没有提供任何帮助,也没有任何指示关于为什麽你的id是无效的。很可爱,不是吗?
SKProduc
t类提供了有关程序标题和描述的本地化版本,但是价格则没有本地化版本。下面是针对此疏忽提供的代码:
1. // SKProduct+LocalizedPrice.h
2.
3. #import <Foundation/Foundation.h>
4. #import <StoreKit/StoreKit.h>
5.
6. @interface SKProduct (LocalizedPrice)
7.
8. @property (nonatomic, readonly) NSString *localizedPrice;
9.
10. @end
11.
1. // SKProduct+LocalizedPrice.m
2.
3. #import "SKProduct+LocalizedPrice.h"
4.
5. @implementation SKProduct (LocalizedPrice)
6.
7. - (NSString *)localizedPrice
8. {
9. NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
10. [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
11. [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
12. [numberFormatter setLocale:self.priceLocale];
13. NSString *formattedString = [numberFormatter stringFromNumber:self.price];
14. [numberFormatter release];
15. return formattedString;
16. }
17.
18. @end
- 加入上述代码,测试一下。你应该在控制台窗口中看见产品信息了。然而更大的可能是,你得到了一个无效的产品id。我下一篇文章将介绍怎样对这个问题进行调试。但是,下面的步骤8有可能是阻碍你前进的障碍。
8. 等待几小时
遵循了上述所有步骤,但是你的产品仍然是无效的?你是否两次,三次,四次不懈努力地确认你是否遵循了上面提到的每个步骤?你是否已经对网上IAP信息少得可怜而感到绝望?
那麽,你应该等待。
你的产品要进入iTunes Connect使得Apple准备好沙箱环境需要一些时间。对于我而言,我是经过了无数次产品无效错误的绝望。而在24小时后,我没有修改任何一行代码,但产品id变为有效。我认为要使产品发佈到Apple的网络系统需要几个小时的时间,但如果你有时间的话,你可以像我一样等上24个小时。
2,购买产品详细步骤:
通过步骤1,成功的获取到了产品的SKProduct描述。支持购买实现一下三点即可。
a,编写代码支持事务(transaction)。
b,在iTunes Connect中添加程序测试用户。
c,在设备中登录你的 iTunes Store 帐号。
详细:
1. 编写代码支持事务
首先注意:你将负责开发产品购买的用户界面。StoreKit 未提供任何与用户界面相关的元素。如果你希望你的购买用户界面与App Store一样,那麽你要自己完成。
下面所有代码都是有关事务处理的后台部分。这是一个单独的类只有一条简单的API以供外部类(比如view controller)调用进行购买。如果你找到将其集成到你程序的购买部分的方法,那麽我推荐你使用类似方桉。
首先,需要遵循 SKPaymentTransactionObserver 协议:
1. // InAppPurchaseManager.h
2.
3. // add a couple notifications sent out when the transaction completes
4. #define kInAppPurchaseManagerTransactionFailedNotification @"kInAppPurchaseManagerTransactionFailedNotification"
5. #define kInAppPurchaseManagerTransactionSucceededNotification @"kInAppPurchaseManagerTransactionSucceededNotification"
6.
7. …
8.
9. @interface InAppPurchaseManager : NSObject <SKProductsRequestDelegate, SKPaymentTransactionObserver>
10. {
11. …
12. }
13.
14. // public methods
15. - (void)loadStore;
16. - (BOOL)canMakePurchases;
17. - (void)purchaseProUpgrade;
18.
19. @end
20.
上面我们定义了两个新的notification,它们将作为购买事务的结果被发送。在上例中我们仍然使用与获取产品描述同一个InAppPurchaseManager类。
1. // InAppPurchaseManager.m
2.
3. #define kInAppPurchaseProUpgradeProductId @"com.runmonster.runmonsterfree.upgradetopro"
4.
5. …
6.
7. #pragma -
8. #pragma Public methods
9.
10. //
11. // call this method once on startup
12. //
13. - (void)loadStore
14. {
15. // restarts any purchases if they were interrupted last time the app was open
16. [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
17.
18. // get the product description (defined in early sections)
19. [self requestProUpgradeProductData];
20. }
21.
22. //
23. // call this before making a purchase
24. //
25. - (BOOL)canMakePurchases
26. {
27. return [SKPaymentQueue canMakePayments];
28. }
29.
30. //
31. // kick off the upgrade transaction
32. //
33. - (void)purchaseProUpgrade
34. {
35. SKPayment *payment = [SKPayment paymentWithProductIdentifier:kInAppPurchaseProUpgradeProductId];
36. [[SKPaymentQueue defaultQueue] addPayment:payment];
37. }
38.
39. #pragma -
40. #pragma Purchase helpers
41.
42. //
43. // saves a record of the transaction by storing the receipt to disk
44. //
45. - (void)recordTransaction:(SKPaymentTransaction *)transaction
46. {
47. if ([transaction.payment.productIdentifier isEqualToString:kInAppPurchaseProUpgradeProductId])
48. {
49. // save the transaction receipt to disk
50. [[NSUserDefaults standardUserDefaults] setValue:transaction.transactionReceipt forKey:@"proUpgradeTransactionReceipt" ];
51. [[NSUserDefaults standardUserDefaults] synchronize];
52. }
53. }
54.
55. //
56. // enable pro features
57. //
58. - (void)provideContent:(NSString *)productId
59. {
60. if ([productId isEqualToString:kInAppPurchaseProUpgradeProductId])
61. {
62. // enable the pro features
63. [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isProUpgradePurchased" ];
64. [[NSUserDefaults standardUserDefaults] synchronize];
65. }
66. }
67.
68. //
69. // removes the transaction from the queue and posts a notification with the transaction result
70. //
71. - (void)finishTransaction:(SKPaymentTransaction *)transaction wasSuccessful:(BOOL)wasSuccessful
72. {
73. // remove the transaction from the payment queue.
74. [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
75.
76. NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:transaction, @"transaction" , nil];
77. if (wasSuccessful)
78. {
79. // send out a notification that we’ve finished the transaction
80. [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerTransactionSucceededNotification object:self userInfo:userInfo];
81. }
82. else
83. {
84. // send out a notification for the failed transaction
85. [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerTransactionFailedNotification object:self userInfo:userInfo];
86. }
87. }
88.
89. //
90. // called when the transaction was successful
91. //
92. - (void)completeTransaction:(SKPaymentTransaction *)transaction
93. {
94. [self recordTransaction:transaction];
95. [self provideContent:transaction.payment.productIdentifier];
96. [self finishTransaction:transaction wasSuccessful:YES];
97. }
98.
99. //
100. // called when a transaction has been restored and and successfully completed
101. //
102. - (void)restoreTransaction:(SKPaymentTransaction *)transaction
103. {
104. [self recordTransaction:transaction.originalTransaction];
105. [self provideContent:transaction.originalTransaction.payment.productIdentifier];
106. [self finishTransaction:transaction wasSuccessful:YES];
107. }
108.
109. //
110. // called when a transaction has failed
111. //
112. - (void)failedTransaction:(SKPaymentTransaction *)transaction
113. {
114. if (transaction.error.code != SKErrorPaymentCancelled)
115. {
116. // error!
117. [self finishTransaction:transaction wasSuccessful:NO];
118. }
119. else
120. {
121. // this is fine, the user just cancelled, so don’t notify
122. [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
123. }
124. }
125.
126. #pragma mark -
127. #pragma mark SKPaymentTransactionObserver methods
128.
129. //
130. // called when the transaction status is updated
131. //
132. - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
133. {
134. for (SKPaymentTransaction *transaction in transactions)
135. {
136. switch (transaction.transactionState)
137. {
138. case SKPaymentTransactionStatePurchased:
139. [self completeTransaction:transaction];
140. break;
141. case SKPaymentTransactionStateFailed:
142. [self failedTransaction:transaction];
143. break;
144. case SKPaymentTransactionStateRestored:
145. [self restoreTransaction:transaction];
146. break;
147. default:
148. break;
149. }
150. }
151. }
-
要测试上面的新代码,你还需要编写调用 loadStore, canMakePurchases 以及 purchaseProUpgrade 方法的代码。
有关上述代码的详细解释,请参考官方 In App Purchase Programming Guide (IAP编程指南)
上述代码有几个部分是针对我的程序的。例如,在 provideContent:中,NSUserDefaults 中的@”isProUpgradePurchased” BOOL 字段被设定为 YES。程序的其他部分将检查此BOOL值以确定是否需要启动专业版功能。如果你正好也要实现免费升级专业版的功能,那麽你可以使用同样的方法。
2. 添加测试用户
为测试上述代码,你需要在 iTunes Connect 中创建测试用户以对IAP功能进行测试。你可以使用测试帐号购买产品而不被Apple收取费用。
按以下步骤创建测试用户:
登录到 http://developer.apple.com/iphone 进入 iTunes Connect
选择iTunes Connect首页中的 “Manage Users”
选择 “In App Purchase Test User”
选择 “Add New User”
填入用户信息. 所有信息都不必是合法的。建议使用虚假简短的email地址及简短的密码。
选择 “Save”
测试时你需要输入这些email地址和密码。
<1 在添加Test User之前,应该已经设置好ITunes Connect账户中关于Purchase Contract的信息。至少应该点击contract 的request按钮,让contract置于pending in process的状态。
完成上面的操作后,你才会在刚进入Manage User界面的时候,能选择创建Test User。
一、在现在的IAP测试中,已经不需要提交程序再让开发者把程序状态设置“Developer Reject”了,只需要程序 到”prepare for upload“就可以了。
二、在IOS5,已经可以在模拟器中测试,调试IAP这个功能了。
3. 在你的设备中退出登录
在进行程序购买功能测试前,你必须在你的设备中退出iTunes Store。遵循以下步骤:
打开Settings App
点击 “Store” 行
点击 “Sign Out”
4. 购买测试
现在,终于可以开始进行IAP功能的测试了。测试很简单:
运行你设备中的程序
进行购买
当程序提示输入用户名和密码时,输入参数用户的信息
如果你使用同一账户进行购买时,系统将提示你已经购买了此产品。按“Yes”就可以再次下载此产品。
注:向apple服务器提交验证的未证方式:
NSDictionary *tempDict = [NSDictionary dictionaryWithObject:[transaction transactionReceipt] forKey:@"receipt-data"];
NSString *josnValue = [tempDict JSONRepresentation];
NSURL *sandboxStoreURL = [[NSURL alloc] initWithString: @"https://sandbox.itunes.apple.com/verifyReceipt"];
NSData *postData = [NSData dataWithBytes:[josnValue UTF8String] length:[josnValue length]];
NSMutableURLRequest *connectionRequest = [NSMutableURLRequest requestWithURL:sandboxStoreURL];
[connectionRequest setHTTPMethod:@"POST"];
[connectionRequest setTimeoutInterval:120.0];
[connectionRequest setCachePolicy:NSURLRequestUseProtocolCachePolicy];
[connectionRequest setHTTPBody:postData];
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
NSURLConnection *connection = [[NSURLConnection alloc]
initWithRequest:connectionRequest
delegate:self];
[connection release];
-----------------------------------------------------------------------------------------------------------------------
简单地说:
1. iTunes Connect -> Manage Your Applications -> Manage In-App Purchases,创建你需要的Product,一般来说分为:消耗型、非消耗型、自动再生订阅、免费订阅、非自动再生订阅几种。消耗型一般适合于游戏里的虚拟物品,以及一次性使用的服务;非消耗型适用于功能解锁等
2. iTunes Connect 建立一个test user,专门用来测试in-App Purchases的,不会发生实际的购买
3. 实现IAP有内建模型和服务器模型两种,简单点的话直接内建模型,应用内搞定;要想安全一些,防破解高一些的话,就必须使用服务器模型了。
应用内建模型:
In App Purchase为创建产品提供了一种通用的机制,如何操作将由你负责。当你设计程序的时候,有以下几点需要注意:
1. 你必须提供电子类产品和服务。不要使用In App Purchase 去出售实物和实际服务。
2. 不能提供代表中介货币的物品,因为让用户知晓他们购买的商品和服务是很重要的。
服务器模型:
2. 服务器类型
使用这终方式,要提供另外的服务器将产品发送给程序。 服务器交付适用于订阅、内容类商品和服务,因为商品可以作为数据发送,而不需改动程序束。 例如,一个游戏提供的新的内容(关卡等)。 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》文档中有相关的提示说明。
虽然非消耗性商品可以用内置模式来恢复,订阅类商品必须通过服务器来恢复。你要负责纪录订阅信息、恢复数据。
消耗类商品也可以通过服务器方式来纪录。例如,由服务器提供的一项服务, 你可能需要用户在多个设备上重新获得结果。
首先确保项目链接了StoreKit.framework,应用中添加Store的详细步骤如下:
-
1. 定义应用需要递送的products。
Store Kit对products有一些限制,不允许应用对自己打补丁,或者下载额外的代码。products要么已经在应用的现有代码中,要么从远程服务器下载数据文件来实现。如果应用增加特性需要修改现有代码,必须发布一个新版本的应用。
2. 在iTunes Connect中为每个product注册详细信息
每次应用Store要增加一个新的product,都需要先在iTunes Connect中进行注册。每个product都需要一个唯一的ID字符串。App Store使用这个字符串来查找product信息以及处理支付请求。product ID特定于iTunes Connect账号,注册的方式与注册应用类似。
3. 确定系统能够处理支付
用户可以禁止应用内购买,因此你的应用需要先检查当前是否支持应用内购买。应用可以在显示Store给用户之前,或者在实际发起购买请求之前,进行这项检查。后者允许用户查看能够购买的products,即使应用内购买当前被禁止。
if
([SKPaymentQueue canMakePayments])
{
...
// 向用户显示Store
}
else
{
...
// 警告用户当前禁止应用内购买
}
4. 获取products的信息
应用创建一个SKProductsRequest对象,并初始化为一组你想要销售的product ID,添加一个delegate处理请求返回结果,然后就可以发起这个请求。响应结果保存了所有合法的products的本地化信息。应用必须首先获得product的信息,然后才能创建payment请求。
SKProductsResponse对象为App Store返回的响应信息。里面包含两个列表(当然是NSArray了):一是经过验证有效的商品,
@property(nonatomic, readonly) NSArray *products
另外一个是无法被识别的商品信息:
@property(nonatomic, readonly) NSArray * invalidProductIdentifiers
有几种原因将造成商品标识无法被识别,如拼写错误(当然),被标记为不可出售(unavailable for sale),或是对商品信息的改变没有传送到所有App Store的服务器。(这个原因不是很清楚,再议)。
- (
void
) requestProductData
{
SKProductsRequest *request= [[SKProductsRequest alloc]
initWithProductIdentifiers: [
NSSet
setWithObject:
kMyFeatureIdentifier]];
request.delegate =
self
;
[request start];
}
- (
void
)productsRequest:(SKProductsRequest *)request
didReceiveResponse:(SKProductsResponse *)response
{
NSArray
*myProduct = response.products;
// 把信息显示到Store界面
[request autorelease];
}
5. 增加一个用户界面,显示products给用户
Store Kit不提供用户界面类,如何显示Store给用户是应用的事情。
6. 注册一个transaction observer到payment队列
应用实例化一个transaction observer,并将其注册到payment队列。
?
|
如前所述,应用最好在启动时注册observer。交易完成之前应用退出,App Store也仍然记得这些交易。启动时注册observer确保所有之前排队交易的结果都能够被应用接收到。
7. 在应用的MyStoreObserver对象中实现paymentQueue:updatedTransactions: 方法
observer的paymentQueue:updatedTransactions: 方法在新交易被创建或更新时都会被调用
?
购买商品 收集支付信息 要收集支付信息, 你的程序可以创建一个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自动恢复。 要恢复这些商品,你必须在用户购买这些商品时,在你自己的服务器上记录这些交易信息, 并且为用户的设备提供恢复交易信息的机制。
8. observer在用户成功购买后提供相应的product
?
|
成功的交易包含一个transactionIdentifier属性和一个transactionReceipt属性,记录了已处理支付的详细信息。应用不需要对这些信息做任何处理。当然你可能希望记录这些信息并为交易建立一个审(我们的)查跟踪(audit trail)。如果使用服务器来递送内容,应用可以把receipt发送到服务器,然后由服务器向App Store验证该交易。
一旦你完成交付product给用户,应用必须调用finishTransaction: 来完成交易,交易将从payment队列中移除。为了确保products不会丢失,应用应该在调用finishTransaction: 之前交付内容。
9. 处理还原购买,结束交易
?
|
这个方法类似于上面的购买。还原购买是一个新的交易,拥有不同的transaction ID和receipt。你可以单独保存这些信息,并建立审(我们的)查跟踪。但是当完成交易时,你还是要还原原始的交易,那里面保存了实际的payment对象和product ID。
10. 处理失败购买,结束交易
?
|
通常交易失败都是用户决定不购买。应用可以读取失败交易的error域,从而了解为何交易失败。
对于失败的交易,应用唯一需要做的是从队列中移除它。如果应用在交易失败后显示一个对话框告诉用户交易出错,应该避免在用户主动取消时也显示该错误。
11. 做完上面所有以后,你就可以显示用户界面。当用户在Store中选择一项时,应用就创建一个payment对象,并将其添加到payment队列中。
?
1 2 | |
如果Store提供一次购买多个product的功能,你可以创建一个payment对象,并指定quantity属性
?
|
以上的代码示例主要用于内建product模型。如果你的应用使用服务器来交付内容,你需要设计和实现iOS应用与服务器之间通信的协议。你的服务器还需要在交付products给应用之前,先验证receipts。
BUG测试:
真正和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.
-----------------------------------------------------------
原文地址:javascript:void(0)
1,获取"产品付费数量等于0这个问题"的原因
为什么每次都返回数量等于0??
其实有童鞋已经找到原因了,原因是你在 ItunesConnect 里的 “Contracts, Tax, and Banking ”没有设置账户信息。
2,如何恢复iap产品,其实博文已经给出了。这里再详细说下:
首先向AppStore请求恢复交易:
|
然后当用户输入正确的appStore账号密码后,进入
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions//交易结果
进入上面函数中的
|
然后我们再以下重写函数中处理即可!
- (void) restoreTransaction: (SKPaymentTransaction *)transaction
在iOS里内嵌收费,那么分为以下几步:
【提示:以下创建App部分内容,你不用非要等项目能打包了才开始做,可以随时并且随便的创建个测试项目即可,因为嵌入付费并不要求上传App的ipa包的!!】
第一步:你需要在iTunesConnect中创建个新的App,然后为这个App设置一些产品(付费道具)等;
- iTunesConnect是苹果提供的一个平台,主要提供AP发布和管理App的,最重要的功能是创建管理项目信息,项目付费产品(道具)管理、付费的测试账号、提交App等等,这里就简单介绍这么多,关于产品一词在此我们可以理解成游戏道具即可;在苹果看来所有付费都属于产品
OK,打开iTunesConnect网站:https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa (注意:企业级的用户必须使用公司主开发者账号登陆才可!)
成功登陆后的页面如下:
Contracts, Tax, and Banking : 管理银行账号、联系人以及税等等;这里要根据提示完成对应的信息填写!一定要详细填写喔~
Manage Users :管理用户的,比如主账号以及测试付费的(测试App)账号;
Manage Your Applictions:管理应用程序的,你所有发布的应用和每个应用的状态都在这里面;
选择Manage Your Applictions选项,然后新建一个项目:【Add New App】,根据提示来填写吧,这里就不细致说明了~
创建好一个App之后,在点击Manage Your Applictions后的界面应该如下:
(注意:这里的Bundle ID一定要跟你的项目中的info.plist中的Bundle ID保证一致!!!!)
这里可以管理你的项目的信息、状态、是否嵌入GameCenter等等选项,那么本章我们重点介绍如何使用IAp沙盒测试程序内付费,所以这里我们点击右上角的”Manage In-App Purchases“选项进入创建产品(游戏道具)界面如下:
上图中的下方看到Himi创建过的四个产品(道具)了,你可以点击”Create New“选项新建一个产品(付费道具),点击新建如下界面:
上图中Himi没有截图出所有的选项,这里大概介绍下,这个界面是选择你的消费道具的种类,种类说明如下:
类型选择有四种选择:
1.Consumable(消耗品): 每次下载都需要付费;
2.Non-consumable(非消耗品): 仅需付费一次;
3.Auto-Renewable Subscriptions:自动订阅;
4.Free Subscription:免费订阅
最下方是你沙盒测试的截图,暂且不管即可;
Consumable选项,比如很多游戏都是购买金币啦这样子就可以选择这个;然后出现如下界面:
Reference Name: 付费产品(道具的)参考名称
Product ID(产品ID): 你产品的唯一id。通常格式是 com.xx.yy,但它可以是任何形式,不要求以程序的App ID作为前缀。
Add Language: 添加产品名称与描述语言;
Price Tier:选择价格,这里你选择价格后,会出现如上图最下方的价格对照表
Screenshot(截屏): 展示你产品的截屏。(这个直接无视,测试App务必要管这个的)
Product ID(产品ID)可以创建多个,比如我想游戏中分为0.99$ 、1.99$等道具那就创建对应多个产品ID!
我们填写好了”Reference Name“与”Product ID“以及”Price Tier“后,点击”Add Language“选项然后出现如下界面:
上图中的选项:
Language:语言
Displayed Name(显示名称): 用户看到的产品名称。
Description(描述): 对产品进行描述。
Ok,一路 Save保存回到”Manage In-App Purchases“界面中会看到我们新建的产品(道具)如下:
大家可以看到新建的产品(道具)ID:这里Himi创建的产品ID是com.himi.wahaha ,这里要记住这个产品ID哦~
第二步:申请测试账号,利用沙盒测试模拟AppStore购买道具流程!
回到itunesconnect主页中,选择“Manage Users”然后选择“Test User”,然后出现的界面如下图:
这里Himi已经创建了两个测试账号了,点击界面中的 “Add New User”进行创建即可;记住账号和密码哈,记不住就删掉重新建 娃哈哈~(切记:不能用于真正的AppStore中使用此账号,不仅不能用,而且一旦AppStore发现后果你懂得~)
第三步:在项目中申请购买产品代码以及监听;
这里关于购买的代码部分呢,我都有备注的,Himi这里就不详细讲解了,Himi只是在代码后介绍几点值得注意的地方:
这里Himi是新建的一个Cocos2d的项目,然后给出HelloWorldLayer.h以及HelloWorldLayer.m的全部代码,所有购买代码也全在里面也对应有Himi的注释!
HelloWorldLayer.h
[cpp] view plain copy
1. //
2. // HelloWorldLayer.h
3. // buytest
4. //
5. // Created by 华明 李 on 11-10-29.
6. // Copyright Himi 2011年. All rights reserved.
7. //
8.
9.
10. // When you import this file, you import all the cocos2d classes
11. #import "cocos2d.h"
12. #import <UIKit/UIKit.h>
13.
14. #import <StoreKit/StoreKit.h>
15. enum{
16. IAP0p99=10,
17. IAP1p99,
18. IAP4p99,
19. IAP9p99,
20. IAP24p99,
21. }buyCoinsTag;
22.
23. @interface HelloWorldLayer : CCLayer<SKProductsRequestDelegate,SKPaymentTransactionObserver>
24. {
25. int buyType;
26. }
27.
28. +(CCScene *) scene;
29. - (void) requestProUpgradeProductData;
30. -(void)RequestProductData;
31. -(bool)CanMakePay;
32. -(void)buy:(int)type;
33. - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions;
34. -(void) PurchasedTransaction: (SKPaymentTransaction *)transaction;
35. - (void) completeTransaction: (SKPaymentTransaction *)transaction;
36. - (void) failedTransaction: (SKPaymentTransaction *)transaction;
37. -(void) paymentQueueRestoreCompletedTransactionsFinished: (SKPaymentTransaction *)transaction;
38. -(void) paymentQueue:(SKPaymentQueue *) paymentQueue restoreCompletedTransactionsFailedWithError:(NSError *)error;
39. - (void) restoreTransaction: (SKPaymentTransaction *)transaction;
40. -(void)provideContent:(NSString *)product;
41. -(void)recordTransaction:(NSString *)product;
42. @end
43. HelloWorldLayer.m
[cpp] view plain
copy
1. //
2. // IapLayer.m
3. //
4. // Created by Himi on 11-5-25.
5. // Copyright 2011年 李华明 . All rights reserved.
6. //
7.
8. #import "HelloWorldLayer.h"
9. #define ProductID_IAP0p99 @"com.buytest.one"//$0.99
10. #define ProductID_IAP1p99 @"com.buytest.two" //$1.99
11. #define ProductID_IAP4p99 @"com.buytest.three" //$4.99
12. #define ProductID_IAP9p99 @"com.buytest.four" //$19.99
13. #define ProductID_IAP24p99 @"com.buytest.five" //$24.99
14.
15. @implementation HelloWorldLayer
16. +(CCScene *) scene
17. {
18. CCScene *scene = [CCScene node];
19. HelloWorldLayer *layer = [HelloWorldLayer node];
20. [scene addChild: layer];
21. return scene;
22. }
23. -(id)init
24. {
25. if ((self = [super init])) {
26. CGSize size = [[CCDirector sharedDirector] winSize];
27. "Icon.png"];
28. [iap_bg setPosition:ccp(size.width/2,size.height/2)];
29. [self addChild:iap_bg z:0];
30. //---------------------
31. //----监听购买结果
32. [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
33. //申请购买
34. /*
35. enum{
36. IAP0p99=10,
37. IAP1p99,
38. IAP4p99,
39. IAP9p99,
40. IAP24p99,
41. }buyCoinsTag;
42. */
43. [self buy:IAP24p99];
44. }
45. return self;
46. }
47.
48. -(void)buy:(int)type
49. {
50. buyType = type;
51. if ([SKPaymentQueue canMakePayments]) {
52. //[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
53. [self RequestProductData];
54. "允许程序内付费购买");
55. }
56. else
57. {
58. "不允许程序内付费购买");
59. "Alert"
60. "You can‘t purchase in app store(Himi说你没允许应用程序内购买)"
61. "Close(关闭)",nil) otherButtonTitles:nil];
62.
63. [alerView show];
64. [alerView release];
65.
66. }
67. }
68.
69. -(bool)CanMakePay
70. {
71. return [SKPaymentQueue canMakePayments];
72. }
73.
74. -(void)RequestProductData
75. {
76. "---------请求对应的产品信息------------");
77. NSArray *product = nil;
78. switch (buyType) {
79. case IAP0p99:
80. product=[[NSArray alloc] initWithObjects:ProductID_IAP0p99,nil];
81. break;
82. case IAP1p99:
83. product=[[NSArray alloc] initWithObjects:ProductID_IAP1p99,nil];
84. break;
85. case IAP4p99:
86. product=[[NSArray alloc] initWithObjects:ProductID_IAP4p99,nil];
87. break;
88. case IAP9p99:
89. product=[[NSArray alloc] initWithObjects:ProductID_IAP9p99,nil];
90. break;
91. case IAP24p99:
92. product=[[NSArray alloc] initWithObjects:ProductID_IAP24p99,nil];
93. break;
94.
95. default:
96. break;
97. }
98. NSSet *nsset = [NSSet setWithArray:product];
99. SKProductsRequest *request=[[SKProductsRequest alloc] initWithProductIdentifiers: nsset];
100. request.delegate=self;
101. [request start];
102. [product release];
103. }
104. //<SKProductsRequestDelegate> 请求协议
105. //收到的产品信息
106. - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
107.
108. "-----------收到产品反馈信息--------------");
109. NSArray *myProduct = response.products;
110. "产品Product ID:%@",response.invalidProductIdentifiers);
111. "产品付费数量: %d", [myProduct count]);
112. // populate UI
113. for(SKProduct *product in myProduct){
114. "product info");
115. "SKProduct 描述信息%@", [product description]);
116. "产品标题 %@" , product.localizedTitle);
117. "产品描述信息: %@" , product.localizedDescription);
118. "价格: %@" , product.price);
119. "Product id: %@" , product.productIdentifier);
120. }
121. SKPayment *payment = nil;
122. switch (buyType) {
123. case IAP0p99:
124. //支付$0.99
125. break;
126. case IAP1p99:
127. //支付$1.99
128. break;
129. case IAP4p99:
130. //支付$9.99
131. break;
132. case IAP9p99:
133. //支付$19.99
134. break;
135. case IAP24p99:
136. //支付$29.99
137. break;
138. default:
139. break;
140. }
141. "---------发送购买请求------------");
142. [[SKPaymentQueue defaultQueue] addPayment:payment];
143. [request autorelease];
144.
145. }
146. - (void)requestProUpgradeProductData
147. {
148. "------请求升级数据---------");
149. "com.productid"];
150. SKProductsRequest* productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
151. productsRequest.delegate = self;
152. [productsRequest start];
153.
154. }
155. //弹出错误信息
156. - (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
157. "-------弹出错误信息----------");
158. "Alert",NULL) message:[error localizedDescription]
159. "Close",nil) otherButtonTitles:nil];
160. [alerView show];
161. [alerView release];
162. }
163.
164. -(void) requestDidFinish:(SKRequest *)request
165. {
166. "----------反馈信息结束--------------");
167.
168. }
169.
170. -(void) PurchasedTransaction: (SKPaymentTransaction *)transaction{
171. "-----PurchasedTransaction----");
172. NSArray *transactions =[[NSArray alloc] initWithObjects:transaction, nil];
173. [self paymentQueue:[SKPaymentQueue defaultQueue] updatedTransactions:transactions];
174. [transactions release];
175. }
176.
177. //<SKPaymentTransactionObserver> 千万不要忘记绑定,代码如下:
178. //----监听购买结果
179. //[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
180.
181. - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions//交易结果
182. {
183. "-----paymentQueue--------");
184. for (SKPaymentTransaction *transaction in transactions)
185. {
186. switch (transaction.transactionState)
187. {
188. case SKPaymentTransactionStatePurchased://交易完成
189. [self completeTransaction:transaction];
190. "-----交易完成 --------");
191. "不允许程序内付费购买");
192. "Alert"
193. "Himi说你购买成功啦~娃哈哈"
194. "Close(关闭)",nil) otherButtonTitles:nil];
195.
196. [alerView show];
197. [alerView release];
198. break;
199. case SKPaymentTransactionStateFailed://交易失败
200. [self failedTransaction:transaction];
201. "-----交易失败 --------");
202. "Alert"
203. "Himi说你购买失败,请重新尝试购买~"
204. "Close(关闭)",nil) otherButtonTitles:nil];
205.
206. [alerView2 show];
207. [alerView2 release];
208. break;
209. case SKPaymentTransactionStateRestored://已经购买过该商品
210. [self restoreTransaction:transaction];
211. "-----已经购买过该商品 --------");
212. case SKPaymentTransactionStatePurchasing: //商品添加进列表
213. "-----商品添加进列表 --------");
214. break;
215. default:
216. break;
217. }
218. }
219. }
220. - (void) completeTransaction: (SKPaymentTransaction *)transaction
221.
222. {
223. "-----completeTransaction--------");
224. // Your application should implement these two methods.
225. NSString *product = transaction.payment.productIdentifier;
226. if ([product length] > 0) {
227.
228. "."];
229. NSString *bookid = [tt lastObject];
230. if ([bookid length] > 0) {
231. [self recordTransaction:bookid];
232. [self provideContent:bookid];
233. }
234. }
235.
236. // Remove the transaction from the payment queue.
237.
238. [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
239.
240. }
241.
242. //记录交易
243. -(void)recordTransaction:(NSString *)product{
244. "-----记录交易--------");
245. }
246.
247. //处理下载内容
248. -(void)provideContent:(NSString *)product{
249. "-----下载--------");
250. }
251.
252. - (void) failedTransaction: (SKPaymentTransaction *)transaction{
253. "失败");
254. if (transaction.error.code != SKErrorPaymentCancelled)
255. {
256. }
257. [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
258.
259.
260. }
261. -(void) paymentQueueRestoreCompletedTransactionsFinished: (SKPaymentTransaction *)transaction{
262.
263. }
264.
265. - (void) restoreTransaction: (SKPaymentTransaction *)transaction
266.
267. {
268. " 交易恢复处理");
269.
270. }
271.
272. -(void) paymentQueue:(SKPaymentQueue *) paymentQueue restoreCompletedTransactionsFailedWithError:(NSError *)error{
273. "-------paymentQueue----");
274. }
275.
276.
277. #pragma mark connection delegate
278. - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
279. {
280. "%@", [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]);
281. }
282. - (void)connectionDidFinishLoading:(NSURLConnection *)connection{
283.
284. }
285.
286. - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
287. switch([(NSHTTPURLResponse *)response statusCode]) {
288. case 200:
289. case 206:
290. break;
291. case 304:
292. break;
293. case 400:
294. break;
295. case 404:
296. break;
297. case 416:
298. break;
299. case 403:
300. break;
301. case 401:
302. case 500:
303. break;
304. default:
305. break;
306. }
307. }
308.
309. - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
310. "test");
311. }
312.
313. -(void)dealloc
314. {
315. //解除监听
316. [super dealloc];
317. }
318. @end
代码注释的相当清楚了,没有什么可解释的,这里说几点值得注意的地方:
1.添加对应对应代码时不要忘记,添加框架 StoreKit.framework,如何添加框架请看我的博文【iOS-Cocos2d游戏开发之十四】音频/音效/视频播放(利用Cocos2D-iPhone-Extensions嵌入Cocos2d进行视频播放!)!
2. 越狱机器无法沙盒测试!模拟器的话,Himi用4.3模拟器不可以,因为提示没有开启程序内付费- -(我都没看到模拟器有store的选项,so~);但是使用iOS5的模拟器可以测试沙盒,但是执行的顺序会有些问题,但是还没真机的童鞋可以使用,建议一切以真机实测为准
3. 千万不要忘记在iTunesConnect中创建的App Bundle ID一定要跟你的项目中的info.plist中的Bundle ID保证一致!!!!
4. 以上代码中你需要修改的就是我在HelloWorldLayer.m类中的宏定义的Product ID(产品ID),例如Himi刚才新建了一个产品ID是“com.himi.wahaha"
然后我运行项目截图如下以及运行控制台打印的信息如下:
点击Buy之后运行截图以及打印信息:
输入测试账号密码后以及打印信息:
这里Himi最后一张截图是没有购买成功,这里Himi是故意截图出来的,原因就是想告诉童鞋们:
如果你的产品信息能够正常得到,但是始终无法成功的话,不要着急,因为你的产品要进入iTunes Connect,并且Apple准备好沙箱环境需要一些时间。Himi之前遇到过,然后在过了段时间后我没有修改任何一行代码,但产品ID变为有效并能成功购买。=。 =郁闷ing~~ 其实要使产品发布到Apple的网络系统是需要一段时间的,so~这里别太着急!
越狱机器无法正常测试沙盒的喔~
顺便提示一下:Bundle ID 尽可能与开发者证书的app ID 一致。
在程序中添加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,其地址为:
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 购买信息的键:
键名 | 描述 |
quantity | 购买商品的数量。对应SKPayment对象中的quantity属性 |
product_id | 商品的标识,对应SKPayment对象的productIdentifier属性。 |
transaction_id | 交易的标识,对应SKPaymentTransaction的transactionIdentifier属性 |
purchase_date | 交易的日期,对应SKPaymentTransaction的transactionDate属性 |
original_-transaction_id | 对于恢复的transaction对象,该键对应了原始的transaction标识 |
original_purchase_-date | 对于恢复的transaction对象,该键对应了原始的交易日期 |
app_item_id | App Store用来标识程序的字符串。一个服务器可能需要支持多个server的支付功能,可以用这个标识来区分程序。链接sandbox用来测试的程序的不到这个值,因此该键不存在。 |
version_external_-identifier | 用来标识程序修订数。该键在sandbox环境下不存在 |
bid | iPhone程序的bundle标识 |
bvrs | iPhone程序的版本号 |
测试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"];
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版本的应用程序到你的测试机器上进行测试。
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以上编写的。
--------------------------------------------------------------------
成为ios开发者最大的好处就是,你编写的应用程序会有很多方式可以赚钱。比如,收费版,免费挂广告版,还有就是程序内置购买。
程序内置购买会让你爱不释手,主要有以下原因:
- 除了程序本身的下载收费以外,你还可以赚更多的钱。一些用户愿意为那些额外的功能花费大量的金钱。
- 你可以免费发布你的程序(这样的话,用户就可以任意下载了),如果他们喜欢这个程序的话,那么就会有人愿意购买额外功能。
- 在你做完一个程序的时候,你可以在以后的发布版中添加更多的功能,然后这些功能可以用内置购买,这样的话,你就不用再重新制作另一个程序了。
我最近正在制作的一个程序里面,我就决定先把程序免费(其中只包含一个故事),然后把更多的故事放在in-app purchase里面。
在这篇教程里面,你将会学到如何使用程序内置付费来解琐本地程序里面的内容,我将向你展示一些技巧,用来应付使用程序内置购买功能时的一些异步特性。请谨慎采纳这些建议,因为我的程序也还在开发之中,但是,随着我的知识的积累,我会逐步更新教程内容以确保不误人子弟。
这篇教程的前提条件你需要熟悉基本的ios编程概念,如果你还是一个ios开发新手,可以先参考这些教程。
In App Rage
那么,本教程将制作一个怎样的程序呢?好吧,在揭晓答案之前,我先介绍一些背景情况。。。
rage comics这玩意儿非常着迷。如果你以前从没听说过它,让我向你们介绍一下吧。它们实际上就是一些非常有趣的漫画,里面有些人非常搞笑和搞怪的人和事。
因此,这篇教程,我们想要做一个非常小巧的应用,叫做“In App Rage”,在这个程序里面,用户可以使用内置购买来获得一些漫画。但是,在我们开始编码之前,我们需要先用ios Developer Center和iTunes Connect来为本程序创建一个入口点。
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:
- Product ID:
- Type:
- 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:
好吧,接下来让我们来实现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 写的一些非常不错的文章。