アプリ内課金 実装編

プロダクト取得のコードが書けてテストも成功したら、今度は購入処理のコードを記述してみる。



  • まず、ヘッダにSKPaymentTransactionObserverを追加する。僕の場合はAlertViewも使っているのでこんな感じ

    @interface InAppPurchaseViewController : UIViewController
    <SKProductsRequestDelegate,SKPaymentTransactionObserver,UIAlertViewDelegate>{

  • ViewDidloadでPaymentQueueObserverになる処理を書く。Delegate=self的なノリ。これにより、一連の購入処理をハンドリングして逐次オブザーバーメソッドが呼ばれるようになる。

    	//SKPaymentQueueのオブザーバーとして登録
    	[[SKPaymentQueue defaultQueue]addTransactionObserver:self];

  • 次にアプリケーションが購入可能かどうか調べる。
    これには+(BOOL)SKPaymentQueueで調べる。

    //デバイスの機能制限かかっているかどうか調べる事ができる。
    if([SKPaymentQueue canMakePayments]==NO){
    	UIAlertView *alert =[[UIAlertView alloc]initWithTitle:@""
    	  message:@"iphoneの機能が制限されています。[設定]>[一般]>[機能制限]>[App内での購入]をONにいていただいたうえ再度お試しください"
    		delegate:self
    		cancelButtonTitle:@"YES"
    		otherButtonTitles:nil];
    		alert.tag  =1;
    		[alert show];
    		return;
    	}

  • 下のコードではReachablityというクラスを使って、デバイスのネットワークの機能が使えるかどうかも判定している。これはiPhoneSDK開発のレシピを参考にした。
  • 次は-(void)productsRequest:didReceiveResponse:(プロダクトの取得で返ってくるDelegateメソッド)の中で、プロダクトの情報取得のコードとかをごちゃごちゃ書かずに購入処理させる。
    プロダクト情報を表示させるところを[self purchase];に差し替え、purchaseメソッドを実装。このメソッドで購入処理を実行する。

    -(void)purchase{
     [indicator showIndicator:self.view];
     SKPayment *payment = [SKPayment  paymentWithProductIdentifier:product_.productIdentifier];
     [[SKPaymentQueue defaultQueue] addPayment:payment];

  • 購入処理結果はSKPaymentQueueObserverで返ってくる。購入処理を受け取る為に定義されているメソッドは2つあり、paymentQueue:updatedTransactionsとpaymentQueue:removedTransactions:だ。その後、レシートの確認や購入履歴復元の処理を書いたらfinishTransaction:メソッドで購入手続きを完了する。参考にした書籍は木下さんの本が主で、レシートの確認の処理まわりや全体のまとまりはエリカ本のサンプルがGithubにあったのでそれを参考にしました。
  • また実際にリリースしてみると、本番環境でプロダクトの取得がまれに失敗したりするので3回失敗するまでは繰り返しリクエストを投げるようにコーディングしてます。
  • ituneConnectの利用規約が変わったりして、agreeをするのが漏れていたりするとsandboxで成功しても実際のアプリでプロダクトの取得の部分で失敗するようです。
    規約に同意して、さらにitunesconnectから問い合わせをして、24時間後にアプリ内課金が動きだすという悲惨な結果に終わったので注意してください。

InAppPurchaseViewController.m

#define SANDBOX	YES


@implementation InAppPurchaseViewController
@synthesize errString;
@synthesize product = product_;
@synthesize requestCount;


#pragma mark -
#pragma mark viewDidLoad

- (void)viewDidLoad {
	
	[super viewDidLoad];
	
	indicator=[[IndicatorView alloc]init];
	[indicator showIndicator:self.view];
	

	//SKPaymentQueueのオブザーバーとして登録
	[[SKPaymentQueue defaultQueue]addTransactionObserver:self];
	
	Reachability* curReach = [Reachability reachabilityForInternetConnection];
	NSParameterAssert([curReach isKindOfClass:[Reachability class]]);
	
	//ネットワーク判定
	if ([curReach currentReachabilityStatus]==NotReachable){
		UIAlertView *alert =[[UIAlertView alloc]initWithTitle:@""
													  message:@"ネットワークに接続できません。インターネットに接続をご確認のうえ再度お試しください。"
													 delegate:self
											cancelButtonTitle:@"YES"
											otherButtonTitles:nil];
		alert.tag  =1;
		[alert show];
		return;
	}
	//デバイスの機能制限
	if([SKPaymentQueue canMakePayments]==NO){
		UIAlertView *alert =[[UIAlertView alloc]initWithTitle:@""
													  message:@"iphoneの機能が制限されています。[設定]>[一般]>[機能制限]>[App内での購入]をONにいていただいたうえ再度お試しください"
													 delegate:self
											cancelButtonTitle:@"YES"
											otherButtonTitles:nil];
		alert.tag  =1;
		[alert show];
		return;
	}
	
	
	requestCount = 0;//requestCountを初期化
	
	[self productRequest];
	
}
-(void)productRequest{
	//プロダクトIDの集合を生成
	NSSet *productIds;
	productIds = [NSSet setWithObject:PRODUCT];
	
	//SKProductRequestを作成
	SKProductsRequest *preq = [[SKProductsRequest alloc]
							   initWithProductIdentifiers:productIds];
	preq.delegate = self;
	
	requestCount++;
	NSLog(@"%d",requestCount);
	
	//Store Kitへのアクセスを開始
	[preq start];
}

#pragma mark viewWillAppear
- (void)viewWillAppear:(BOOL)animated {
	
	[super viewWillAppear:animated];
}

#pragma mark -
#pragma mark SKProductsRequest Delegate

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
	
	
	if(requestCount>2){
		[indicator removeIndicator];
		errString = [NSMutableString stringWithFormat:@"Error: Could not contact App Store properly, %@", [error localizedDescription]];

		UIAlertView *alert =[[UIAlertView alloc]initWithTitle:@""
												  message:@"エラーが発生しました。お手数ですが再度お試しください"
												 delegate:self
										cancelButtonTitle:@"無視する"
										otherButtonTitles:nil];
		alert.tag  =1;
		[alert show];
	}else {
		[self productRequest];
	}

}

- (void)requestDidFinish:(SKRequest *)request
{
	[request release];
	NSLog(@"Request finished.");
}

-(void)productsRequest:(SKProductsRequest *)request
	didReceiveResponse:(SKProductsResponse *)response{
	
	product_ = [[[response products] lastObject]retain];
	if (!product_)
	{
		if(requestCount>2){
			[indicator removeIndicator];
			//NSlog(@"no product ids:%@",[[response invalidProductIdentifiers] description]);
			errString = [NSMutableString stringWithFormat:@"no product ids:%@",[[response invalidProductIdentifiers] description]];
			UIAlertView *alert =[[UIAlertView alloc]initWithTitle:@""
													  message:@"商品が購入いただけません。お手数ですが開発者へ連絡をしていただきますようお願い申し上げます。"
													 delegate:self
											cancelButtonTitle:@"無視する"
											otherButtonTitles:@"通知する",nil];
			alert.tag  =2;
			[alert show];
			return;
		}else {
			[self productRequest];
			return;
		}

}
	
[indicator removeIndicator];
		

	[self purchase];
}

#pragma mark -
#pragma mark SKPaymentQueue observerMethod

-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray*)transactions{
	
	//購入処理を取得
	for(SKPaymentTransaction* transaction in transactions){
		
		//購入処理の状態を調べる
		const char *sql = "Update UserInfo Set isPurchase =1 Where id = 1";
		switch (transaction.transactionState) {
				
			
			case SKPaymentTransactionStatePurchasing://購入中の場合	
				[self repurchase];
				
				break;
			
			case SKPaymentTransactionStatePurchased://購入処理が成功した場合
				
			//TODO 購入処理
				appDelegate = (WorldAppDelegate *)[[UIApplication sharedApplication] delegate];
				appDelegate.isPurchase = YES;
				
									
				[Coffee Update_stmt:[appDelegate getDBPath] SQL:(char *)sql];
				bt1.hidden = YES;
				
				[self completedPurchaseTransaction:transaction];
				//購入処理完了
				[queue finishTransaction:transaction];
				[indicator removeIndicator];
				
				break;
			
			//購入処理失敗
			case SKPaymentTransactionStateFailed:
				if(requestCount>2){
					[self handleFailedTransaction:transaction]; 
					[indicator removeIndicator];
				}else {
					requestCount++;
					[self purchase];
				}

				break;

			//購入処理復元
			case SKPaymentTransactionStateRestored:
				
			//ロック解除処理
				appDelegate = (WorldAppDelegate *)[[UIApplication sharedApplication] delegate];
				appDelegate.isPurchase = YES;
				const char *sql = "Update UserInfo Set isPurchase =1 Where id = 1";
				
				[Coffee Update_stmt:[appDelegate getDBPath] SQL:(char *)sql];	
				bt1.hidden = YES;
			//購入処理を終了
				[queue finishTransaction:transaction];
				[indicator removeIndicator];
				break;
		
			default:
				NSLog(@"Other transaction");
				
				break;
		}
	}
}

#pragma mark -
#pragma mark payment

- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions
{
}
- (void) handleFailedTransaction: (SKPaymentTransaction *) transaction
{
	[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

- (void) completedPurchaseTransaction: (SKPaymentTransaction *) transaction
{
	
	// PERFORM THE SUCCESS ACTION THAT UNLOCKS THE FEATURE HERE
	
	// Check the receipt
	NSString *json = [NSString stringWithFormat:@"{\"receipt-data\":\"%@\"}", [transaction.transactionReceipt base64Encoding]];
	NSString *urlsting = SANDBOX ? @"https://sandbox.itunes.apple.com/verifyReceipt" : @"https://buy.itunes.apple.com/verifyReceipt";
	NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: urlsting]];
	if (!urlRequest) NOTIFY_AND_LEAVE(@"Error creating the URL Request");
	
	[urlRequest setHTTPMethod: @"POST"];
	[urlRequest setHTTPBody:[json dataUsingEncoding:NSUTF8StringEncoding]];
	
	NSError *error;
	NSURLResponse *response;
	NSData *result = [NSURLConnection sendSynchronousRequest:urlRequest returningResponse:&response error:&error];
	
	NSString *resultString = [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding];
	CFShow(resultString);
	[resultString release];
	
	// Finish transaction
	[[SKPaymentQueue defaultQueue] finishTransaction: transaction]; // do not call until you are actually finished
	
}

#pragma mark Others

- (void) repurchase
{
	[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}

#pragma mark -
#pragma mark alertView Delegate

-(void)alertView:(UIAlertView*)alertView
clickedButtonAtIndex:(NSInteger)buttonIndex {
	
	if (alertView.tag ==1) {
				
		switch (buttonIndex) {
			case 0:
				[self dismissModalViewControllerAnimated:YES];
				break;
			case 1:

			break;
		}
	}else if(alertView.tag == 2) {
		NSString *htmlBody;
		NSString *escapedBody;
		switch (buttonIndex) {
			case 0:
				[self dismissModalViewControllerAnimated:YES];
				break;
			case 1:
				
				
				htmlBody = @"[error-noprodust(shunkan)";
				
				//// First escape the body using a CF call
				escapedBody = 
				[(NSString*)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)htmlBody, NULL, CFSTR("?=&+"), kCFStringEncodingUTF8) autorelease];
				//
				//// Then escape the prefix using the NSString method
				NSString *mailtoPrefix = [@"mailto:oktechinfo@gmail.com?subject=" stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
				//
				//// Finally, combine to create the fully escaped URL string
				NSString *mailtoStr = [mailtoPrefix stringByAppendingString:escapedBody];
				
				// And let the application open the merged URL
				[[UIApplication sharedApplication] openURL:[NSURL URLWithString:mailtoStr]];
				
				break;
		}
	}
}

@end