最近在做一款產(chǎn)品,用到了In-App Purchase,對比Apple的文檔研究了兩天,也找了一些文章,但是沒有一篇是非常詳細(xì)的,因?yàn)槿魏我稽c(diǎn)細(xì)節(jié)不注意,里面都會有坑。所以決定寫一篇詳細(xì)的技術(shù)文章,希望能幫到初次做這個功能的技術(shù)朋友。這里以【訂閱】作為例子介紹。
前期準(zhǔn)備:
iTunesConnect平臺配置
1、配置銀行賬戶信息
【協(xié)議、稅務(wù)和銀行業(yè)務(wù)】
這部分按照頁面指示填寫即可,第一步是收款的銀行卡信息,第二步是Tax信息,主要填寫美國報稅表,這里資料很多,不做贅述,可參考:https://blog.csdn.net/joinclear/article/details/107641680
2,創(chuàng)建內(nèi)購

3,在內(nèi)購群組中創(chuàng)建內(nèi)購項(xiàng)目



以上,內(nèi)購產(chǎn)品就差不多了。
接下來,創(chuàng)建沙盒測試賬號。
4,創(chuàng)建沙盒測試賬號,在Debug版本上,測試內(nèi)購購買。


注意,單選編輯某個賬號,可以設(shè)置訂閱自續(xù)費(fèi)的時間周期,測試期間,一般按照分鐘設(shè)置。
在真機(jī)上測試時,請一定要記住,先把iPhone上appstore的賬號退出登錄。
至此,我們就進(jìn)入代碼階段。
In-App Purchase的代碼實(shí)現(xiàn)細(xì)節(jié)
1,引入StoreKit.framework
注意,app上的內(nèi)購邏輯,不能強(qiáng)制用戶登錄后內(nèi)購,否則會被審核Reject。
2,內(nèi)購界面
內(nèi)購界面一定要做一些具體的說明,一定要帶【恢復(fù)購買】的按鈕。
以我自己的app舉例:

3,邏輯代碼
1)第一步:首先啟動StoreKit的回調(diào)監(jiān)聽
我是將內(nèi)購封裝到了一個單實(shí)例中,而且在
- (BOOL)application:(UIApplication*)applicationdidFinishLaunchingWithOptions:(NSDictionary*)launchOptions
在這個啟動函數(shù)中,第一時間進(jìn)行監(jiān)聽,原因就是如果Apple自動扣費(fèi)后,用戶啟動app后,會收到續(xù)費(fèi)的消息通知,需要準(zhǔn)確接收,來處理用戶的到期時間 。
if ([SKPaymentQueue canMakePayments]) {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
2)發(fā)起購買請求
產(chǎn)品的id,就是在iTunesConnect中創(chuàng)建內(nèi)購時設(shè)置的id字符串。
//發(fā)起產(chǎn)品查詢請求
NSSet*productIds = [[NSSet alloc] initWithArray:@[MONTH_PRODUCTID]];
SKProductsRequest* productReq = [[SKProductsRequest alloc] initWithProductIdentifiers:productIds];
productReq.delegate=self;
[productReq start];
//產(chǎn)品查詢回調(diào),因?yàn)槲揖图恿艘粋€產(chǎn)品,所以我直接取第一個了。
#pragma mark SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
[response.products enumerateObjectsUsingBlock:^(SKProduct * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
[response.invalidProductIdentifiers enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
if (response.products.count > 0) {
SKProduct *product = response.products.firstObject;
SKMutablePayment *payment = [[SKMutablePayment alloc] init];
payment.applicationUsername = @"xxxx可以是用戶ID";
payment.productIdentifier = product.productIdentifier;
payment.quantity = 1;
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
else
{
[SVProgressHUD dismiss];
}
}
3)支付回調(diào)
#pragma mark **SKPaymentTransactionObserver**
// 13.監(jiān)聽購買結(jié)果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction {
for (SKPaymentTransaction *tran in transaction){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
// NSLog(@"交易完成");
// 訂閱特殊處理
if (tran.originalTransaction) {
// 如果是自動續(xù)費(fèi)的訂單,originalTransaction會有內(nèi)容
NSLog(@"自動續(xù)費(fèi)的訂單");
} else {
// 普通購買,以及第一次購買自動訂閱
NSLog(@"普通購買,以及第一次購買自動訂閱");
}
[self completeTransaction:tran];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];// 銷毀本次操作,由本地數(shù)據(jù)庫進(jìn)行記錄和恢復(fù)
//[SVProgressHUD dismiss];
break;
case SKPaymentTransactionStatePurchasing:
//NSLog(@"商品添加進(jìn)列表");
break;
case SKPaymentTransactionStateRestored:
// NSLog(@"已經(jīng)購買過商品");
[self completeTransaction:tran];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
//[SVProgressHUD dismiss];
break;
case SKPaymentTransactionStateFailed:
//NSLog(@"交易失敗");
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
[SVProgressHUD dismiss];
break;
default:
[SVProgressHUD dismiss];
break;
}
}
}
// 交易結(jié)束,當(dāng)交易結(jié)束后還要去appstore上驗(yàn)證支付信息是否都正確
- (void)completeTransaction:(SKPaymentTransaction *)transaction {
[self verifyFinishedTransaction:transaction];
}
4) 驗(yàn)證支付信息是否都正確
兩種方式:
1,客戶端自行校驗(yàn)
2,服務(wù)端校驗(yàn)
原理都是一樣的,我這里用客戶端校驗(yàn)來舉例。
我們來看一下apple的文檔怎么說:

注意Important的描述:
我們在用Debug版本測試也好,apple review團(tuán)隊(duì)在審核也好,都是采用sandbox環(huán)境,所以,apple的建議是,首先去product環(huán)境發(fā)送校驗(yàn)【正式的用戶都會走這一步】,返回結(jié)果中的status如果等于21007【測試用戶走這一步】,說明我們應(yīng)該去sandbox去校驗(yàn),再重新發(fā)送一次去sandbox url去校驗(yàn)。
上代碼
代碼中passwrod 就是上面在iTunesConnect上面管理的共享密鑰,對于自續(xù)費(fèi)的內(nèi)購,密鑰必須設(shè)置,否則會返回錯誤
#define ITMS_SANDBOX_VERIFY_RECEIPT_URL @"https://sandbox.itunes.apple.com/verifyReceipt"
#define ITMS_PRODUCT_VERIFY_RECEIPT_URL @"https://buy.itunes.apple.com/verifyReceipt"
#pragma mark - VerifyFinishedTransaction
-(void)verifyFinishedTransaction:(SKPaymentTransaction *)transaction{
NSData *receiptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
NSString* receipt = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
//NSLog(@"購買結(jié)果票據(jù):%@",receipt);
[[GoGoDB sharedDBInstance] insertNewReceipt:receipt];
if(transaction.transactionState == SKPaymentTransactionStatePurchased ||
transaction.transactionState == SKPaymentTransactionStateRestored){
// 在app上做驗(yàn)證, 僅用于測試
//passwrod 就是在iTunesConnect上面管理的共享密鑰
NSDictionary *body = @{@"receipt-data":receipt,
@"password":@""
};
NSError *error = nil;
NSData *payloadData = [NSJSONSerialization dataWithJSONObject:body
options:NSJSONWritingPrettyPrinted
error: &error];
if(payloadData)
{
//我這里用的是我自己封裝的HTTP Request,請?zhí)鎿Q為你自己的HTTP request
if(verify_request == nil)
{
self.verify_request = [[WebClient alloc] initWithDelegate:self];
}
verify_request._httpMethod = @"POST";
//請求參數(shù)
NSMutableDictionary *param = [NSMutableDictionary dictionary];
[param setValue:ITMS_PRODUCT_VERIFY_RECEIPT_URL forKey:@"baseUrl"];
[param setValue:payloadData forKey:@"Body"];
verify_request._requestParam = param;
IMP_BLOCK_SELF(JCVideoPlayer);
[SVProgressHUD showInfoWithStatus:@"即將完成購買"];
[verify_request requestWithJSONBodySusessBlock:^(id lParam, id rParam) {
NSString *response = lParam;
[block_self processAppleReqRes:response payload:payloadData];
} FailBlock:^(id lParam, id rParam) {
[block_self processAppleReqRes:nil payload:nil];
}];
}
}
else
{
[SVProgressHUD dismiss];
}
}
- (void)processAppleReqRes:(NSString*)response payload:(NSData*)payload{
[SVProgressHUD dismiss];
if(response)
{
NSError *jsonParsingError = nil;
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonParsingError];
// NSLog(@"%@", dict);
int status = [[dict objectForKey:@"status"] intValue];
//NSString *environment = [dict objectForKey:@"environment"];
if(status == 0)
{
User *u = [User sharedUserData:nil];
NSArray *latest_receipt_info = [dict objectForKey:@"latest_receipt_info"];
if(latest_receipt_info && [latest_receipt_info count])
{
NSDictionary *latest = [latest_receipt_info firstObject];
long long ms = [[latest objectForKey:@"expires_date_ms"] longLongValue];
int expires_date_s = (int)(ms/1000);
NSString *product_id = [latest objectForKey:@"product_id"];
if(expires_date_s && [product_id isEqualToString:MONTH_PRODUCTID])
{
u._expiretime = [self getLocalDateFormateUTCDate:expires_date_s];
}
}
NSArray *pending_renewal_info = [dict objectForKey:@"pending_renewal_info"];
if(pending_renewal_info && [pending_renewal_info count])
{
NSDictionary *latest = [latest_receipt_info firstObject];
int auto_renew_status = [[latest objectForKey:@"auto_renew_status"] intValue];
NSString *product_id = [latest objectForKey:@"auto_renew_product_id"];
if([product_id isEqualToString:MONTH_PRODUCTID])
{
u.auto_renew_status = auto_renew_status;
}
}
[UserDefaultsKV saveUser:u];
[self refreshAfterPurchase];
}
else if(status == 21007)
{
[self verifySandboxTransaction:payload];
}
//21007
}
}
- (void) verifySandboxTransaction:(NSData*)payloadData{
if(payloadData)
{
self.verify_request = [[WebClient alloc] initWithDelegate:self];
verify_request._httpMethod = @"POST";
NSMutableDictionary *param = [NSMutableDictionary dictionary];
[param setValue:ITMS_SANDBOX_VERIFY_RECEIPT_URL forKey:@"baseUrl"];
[param setValue:payloadData forKey:@"Body"];
verify_request._requestParam = param;
// [SVProgressHUD show];
IMP_BLOCK_SELF(JCVideoPlayer);
[SVProgressHUD showInfoWithStatus:@"即將完成購買"];
[verify_request requestWithJSONBodySusessBlock:^(id lParam, id rParam) {
NSString *response = lParam;
[block_self processAppleReqRes:response payload:nil];
} FailBlock:^(id lParam, id rParam) {
[block_self processAppleReqRes:nil payload:nil];
}];
}
}
- (NSString *)getLocalDateFormateUTCDate:(int )expires_date_s {
NSDate *date = [NSDate dateWithTimeIntervalSince1970:expires_date_s];
NSDateFormatter *format = [[NSDateFormatter alloc] init];
format.dateFormat = @"yyyy-MM-dd HH:mm:ss";
format.timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
format.timeZone = [NSTimeZone localTimeZone];
NSString *dateString = [format stringFromDate:date];
return dateString;
}
對上面代碼做一些說明:

因?yàn)槭亲詣永m(xù)期,所以有多個收據(jù),
latest_receipt_info這個數(shù)組第一個就是最新的收據(jù),
他的expires_date就是最新的過期時間,和這個判斷就可以知道會員是否過期。對于驗(yàn)證用戶是否取消訂閱,時間是GMT時間,要進(jìn)行轉(zhuǎn)換。
pending_renewal_info , 該字段是續(xù)訂狀態(tài)的說明.
auto_renew_status 為0, 說明已經(jīng)關(guān)閉訂閱。對于退款,解析latest_receipt_info中的交易,
退款后會出現(xiàn)cancellation_date和cancellation_reason字段, 未退款則沒有這兩個字段。
至此,應(yīng)該就基本能理解透徹了。