IAP全称叫做 In App Purchase,在应用内购买,顾名思义就是可以在iPhone应用程序里实现二次购买。当然,对于一些免费的应用,IAP是除了植入广告外的另一种取得收益的方式。自从iPhone SDK3.0更新以来,IAP已经得到广泛的应用。我在开发第一个iPhone游戏中就用到了这种功能,这里就简单说一下我实现的具体过程。
这篇文章是针对那些已经对IAP有一定的认识,只是在实现方面遇到困难的人,所以,很多地方我不会进行很详细的解析。
我在游戏中实现道具购买,它是消费型的(Consumable),即可重复购买。
在代码实现之前,你要登陆自己的开发者账号,创建用于开发测试的Apple ID(ID不要带有*,创建成功会默认启动IAP功能,如果没有,点击ID列表最右侧Config,启用In App Purchase功能)。然后生成相应的profile,这些操作就是生成证书和签名,使你的Xcode跟开发者账号联系起来调试,如果不太懂,可以参考一篇比较详细的文章http://www.cocoachina.com/bbs/read.php?tid=7923&page=1 ,里面还包括发布程序的教程。
如果你的Xcode与开发者账号联系起来,那就继续往下看吧。
用你的开发者账号进入iTunes Connect,点击位于左偏上的Manager Your Applications
如果你还没创建你的应用,请点击左上角的Add New App ,这步我会跳过,如果不会创建你的应用,可以参考http://jamesli.cn/blog/?p=1045
创建成功之后,点击进去,再点击右边第二项 Manage In-App Purchases
点击左上角Create New 添加你的道具,选择Consumable ,根据提示填好相关内容,记住,Product ID是以后进行purchase操作的唯一识别,在代码里也会使用到,相当于主键,而且一旦添加后即使删除了以后也不允许再次使用这一ID(官方建议使用域名的命名模式com.companyname.appname.productid),确保你勾选了Clear for Sale。
提交之后,是不是就剩下代码实现的部分了呢?
答案是否定的,你还需要注册一个调试账号,用于IAP调试,即购买时你要用这个账号密码去购买(真机调试前一定要在设置里注销退出自己的真实APP账号),如果用真实的APP账号购买,你的账号余额可能会无缘无故变少了(嘿嘿,我没试过,不知是不是真的会扣你的钱),当然,你有勇气的话,可以尝试一下。
在用你的开发者账号进入iTunes Connect的页面的左下角有Manage Users选项,点击进去,在Test User里新建一个调试账号。
好了,一切都准备好了吧,打开你的Xcode,开始你的代码实现吧!
由于时间有限,我暂时用一个傻瓜方式(直接贴代码)教你如何实现
我一共使用了ToolStoreObserver ToolStoreManager ToolStore三个类,第一个类是默认的,第二个是半默认的,有一部分需要根据自己情况修改,第三个类是自己新建的,主要用来绘制商城的界面以及响应购买按钮事件,已调用第二个类的函数。
在此之前,先添加一个StoreKit framework包。
在定义全局变量的地方将Product ID赋给一个变量
#define P_Id_2 @"com.companyname.appname.productid"
ToolStoreObserver.h
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
@interface ToolStoreObserver : NSObject<SKPaymentTransactionObserver> {
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions;
- (void) failedTransaction: (SKPaymentTransaction *)transaction;
- (void) completeTransaction: (SKPaymentTransaction *)transaction;
- (void) restoreTransaction: (SKPaymentTransaction *)transaction;
@end
ToolStoreObserver.m
#import "ToolStoreObserver.h"
#import "ToolStoreManager.h"
@implementation ToolStoreObserver
- (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) failedTransaction: (SKPaymentTransaction *)transaction
{
if (transaction.error.code != SKErrorPaymentCancelled)
{
// Optionally, display an error here.
}
[[ToolStoreManager sharedManager] paymentCanceled];
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
- (void) completeTransaction: (SKPaymentTransaction *)transaction
{
[[ToolStoreManager sharedManager] provideContent: transaction.payment.productIdentifier];
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
- (void) restoreTransaction: (SKPaymentTransaction *)transaction
{
[[ToolStoreManager sharedManager] provideContent: transaction.originalTransaction.payment.productIdentifier];
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
@end
ToolStoreManager.h
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
#import "ToolStoreObserver.h"
#import "cocos2d.h"
@protocol ToolStoreKitDelegate <NSObject>
@optional
- (void)productPurchased;
- (void)failed;
-(void)stop;
-(void)start;
@end
@interface ToolStoreManager : NSObject <SKProductsRequestDelegate> {
NSMutableArray *purchasableObjects;
ToolStoreObserver *storeObserver;
double productPrice;
id<ToolStoreKitDelegate> delegate;
}
@property (nonatomic, retain) id<ToolStoreKitDelegate> delegate;
@property (nonatomic, retain) NSMutableArray *purchasableObjects;
@property (nonatomic, retain) ToolStoreObserver *storeObserver;
@property double productPrice;
- (void) requestProductData;
- (void) buyFeature:(int) Id;
-(void)paymentCanceled;
- (void) failedTransaction: (SKPaymentTransaction *)transaction;
-(void) provideContent: (NSString*) productIdentifier;
+ (ToolStoreManager*)sharedManager;
- (BOOL) featurePurchased;
@end
ToolStoreManager.m
#import "ToolStoreManager.h"
@implementation ToolStoreManager
@synthesize purchasableObjects;
@synthesize storeObserver;
@synthesize delegate;
@synthesize productPrice;
// all your features should be managed one and only by StoreManager
static NSString *featureId_2 =P_Id_2;
static NSString *featureId_5 =P_Id_5;
static NSString *featureId_10 =P_Id_10;
BOOL featurePurchased;
static ToolStoreManager* _sharedStoreManager; // self
-(BOOL) featurePurchased {
return NO;
}
+ (ToolStoreManager*)sharedManager
{
@synchronized(self) {
if (_sharedStoreManager == nil) {
[[[self alloc] init]autorelease]; // assignment not done here
_sharedStoreManager.purchasableObjects = [NSMutableArray array];
[_sharedStoreManager requestProductData];
_sharedStoreManager.storeObserver = [[[ToolStoreObserver alloc]init]autorelease];
[[SKPaymentQueue defaultQueue] addTransactionObserver:_sharedStoreManager.storeObserver];
}
}
return _sharedStoreManager;
}
- (void) requestProductData
{
SKProductsRequest *request=[[[SKProductsRequest alloc] initWithProductIdentifiers:
[NSSet setWithObjects:featureId_2,featureId_5,featureId_10,nil]]autorelease];// add any other product here
request.delegate = self;
[request start];
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
[purchasableObjects addObjectsFromArray:response.products];
// populate your UI Controls here
for(int i=0;i<[purchasableObjects count];i++)
{
SKProduct *product = [purchasableObjects objectAtIndex:i];
NSLog(@"Feature: %@, Cost: %f, ID: %@",[product localizedTitle],
[[product price] doubleValue], [product productIdentifier]);
self.productPrice=[[product price] doubleValue];
}
//[request autorelease];
}
- (void) buyFeature:(int) Id
{
NSString* featureId;
if([delegate respondsToSelector:@selector(start)])
[delegate start];
switch (Id) {
case 2:
featureId=featureId_2;
break;
case 5:
featureId=featureId_5;
break;
case 10:
featureId=featureId_10;
break;
default:
featureId=@"";
break;
}
if ([SKPaymentQueue canMakePayments])
{
SKPayment *payment = [SKPayment paymentWithProductIdentifier:featureId];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
else
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:@"お客様のAppStore購入は設定されていません。個人設定を再度お確かめ下さい。"
delegate:self cancelButtonTitle:@"OK" otherButtonTitles: nil];
[alert show];
[alert release];
if([delegate respondsToSelector:@selector(stop)])
[delegate stop];
}
}
-(void)paymentCanceled
{
if([delegate respondsToSelector:@selector(failed)])
[delegate failed];
}
- (void) failedTransaction: (SKPaymentTransaction *)transaction
{
if([delegate respondsToSelector:@selector(failed)])
[delegate failed];
NSString *messageToBeShown = [NSString stringWithFormat:@"Reason:%@, You can try: %@", [transaction.error localizedFailureReason], [transaction.error localizedRecoverySuggestion]];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Unable to complete your purchase" message:messageToBeShown
delegate:self cancelButtonTitle:@"OK" otherButtonTitles: nil];
[alert show];
[alert release];
}
-(void) provideContent: (NSString*) productIdentifier
{
if([_sharedStoreManager.delegate respondsToSelector:@selector(productPurchased)])
[_sharedStoreManager.delegate productPurchased];
//[ToolStoreManager updatePurchases:productIdentifier];
}
- (void)requestDidFinish:(SKRequest *)request{
if (request!=nil) {
request=nil;
}
}
#pragma mark Singleton Methods
+ (id)allocWithZone:(NSZone *)zone
{
@synchronized(self) {
if (_sharedStoreManager == nil) {
_sharedStoreManager = [super allocWithZone:zone];
return _sharedStoreManager; // assignment and return on first allocation
}
}
return nil; //on subsequent allocation attempts return nil
}
- (id)copyWithZone:(NSZone *)zone
{
return self;
}
- (void)dealloc {
[_sharedStoreManager release];
[storeObserver release];
[super dealloc];
}
@end
ToolStore.h
#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>
#import <netdb.h>
#import "SimpleAudioEngine.h"
#import "cocos2d.h"
#import <Foundation/Foundation.h>
#import "ToolStoreManager.h"
#import <SystemConfiguration/SCNetworkReachability.h>
@interface ToolStoreLayer : CCLayerColor <ToolStoreKitDelegate,UIAlertViewDelegate>
{
CCLabelTTF *_label;
CCMenuItem *buy099;//购买0.99刀的道具按钮
int tool_id;
}
@property (nonatomic, retain) CCLabelTTF *label;
@end
@interface ToolStoreScene :CCScene {
ToolStoreLayer *_layer;
}
+(id) scene;
@property(nonatomic, retain) ToolStoreLayer *layer;
@end
ToolStore.m
#import "ToolStore.h"
@implementation ToolStoreScene
@synthesize layer = _layer;
+(id) scene
{
CCScene *scene = [CCScene node];
CCLayer* layer = [ToolStoreScene node];
[scene addChild:layer];
return scene;
}
-(id)init{
if ((self = [super init])) {
self.layer = [ToolStoreLayer node];
[self addChild:_layer];
}
return self;
}
- (void)dealloc {
[ _layer release];
_layer = nil;
[super dealloc];
}
@end
@implementation ToolStoreLayer
@synthesize label = _label;
-(id) init {
if( (self=[super initWithColor:ccc4(0,0,0,0)] )) {
CGSize winSize = [[CCDirector sharedDirector] winSize];
//添加按钮
//购买0.99刀道具按钮
buy099 = [CCMenuItemImage
itemFromNormalImage:@"0.99.png" selectedImage:@"0.99.png" target:self selector:@selector(buy099)];
buy099.position = ccp(winSize.width/6,winSize.height/6);
CCMenu *starMenu = [CCMenu menuWithItems:buy099, nil];
starMenu.position = CGPointMake(CGPointZero.x,CGPointZero.y-10) ;
[self addChild: starMenu z:2];
}
return self;
}
-(void) buy099{
UIAlertView * alert =[[UIAlertView alloc]
initWithTitle:NSLocalizedString(@"Buy two suits of tools? " , @"Buy two suits of tools? ")
message:nil
delegate:self
cancelButtonTitle:NSLocalizedString(@"Buy" , @"Buy") otherButtonTitles:NSLocalizedString(@"Cancel" , @"Cancel"), nil];
alert.tag=2;
[alert show];
[alert release];
}
- (BOOL)isNetworkReachable{
// Create zero addy
struct sockaddr_in zeroAddress;
bzero(&zeroAddress, sizeof(zeroAddress));
zeroAddress.sin_len = sizeof(zeroAddress);
zeroAddress.sin_family = AF_INET;
// Recover reachability flags
SCNetworkReachabilityRef defaultRouteReachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&zeroAddress);
SCNetworkReachabilityFlags flags;
BOOL didRetrieveFlags = SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags);
CFRelease(defaultRouteReachability);
if (!didRetrieveFlags)
{
return NO;
}
BOOL isReachable = flags & kSCNetworkFlagsReachable;
BOOL needsConnection = flags & kSCNetworkFlagsConnectionRequired;
return (isReachable && !needsConnection) ? YES : NO;
}
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
tool_id=alertView.tag;
if (tool_id>1) {
switch (buttonIndex) {
case 0:
if (![self isNetworkReachable]) {
UIAlertView* dialog = [[UIAlertView alloc] init];
[dialog setDelegate:self];
[dialog setMessage:@"Service is unreachable!"];
[dialog addButtonWithTitle:@"ok"];
[dialog show];
[dialog release];
break;
}
[ToolStoreManager sharedManager].delegate =self;
[[ToolStoreManager sharedManager] buyFeature:tool_id];
break;
default:
break;
}
}
}
#pragma ToolStoreManger Delegate
-(void)productPurchased
{
NSLog(@"success!");
UIAlertView* dialog = [[UIAlertView alloc] init];
[dialog setDelegate:self];
[dialog setMessage:@"success!"];
[dialog addButtonWithTitle:@"ok"];
[dialog show];
[dialog release];
//购买成功则保存响应数据
[GameStartLayer saveToolDate];
}
- (void)dealloc {
[_label release];
_label = nil;
[super dealloc];
}
@end
最后,我列出几个要注意的地方
确保你所用来创建Profile的Apple ID启用了In App Purchase功能。
确保你的Apple ID的identifier中没有*。
确保你的bundle ID和你的Apple ID的identifier一致。
确保你的product ID是唯一的。
确保你在代码中所请求的product ID与你在iTunes Connect里添加的一致。
确保你勾选了Clear for Sale。
7.在测试的时候你可能需要等待你的商品添加入Apple的测试沙盒,这个过程可能需要几个小时。
8.在你第一次上传应用程序的时候,确保勾选了需要绑定至该应用程序的商品列表。
9.确保你是在SDK3.0以上编写的。
希望这个教程能给你带来一些收获,期待你出色的iPhone应用 !