iOS In-App Purchase 從0到1【iOS內(nèi)購 Object-c版本】

最近在做一款產(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)購

image

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

image
image
image

以上,內(nèi)購產(chǎn)品就差不多了。

接下來,創(chuàng)建沙盒測試賬號。

4,創(chuàng)建沙盒測試賬號,在Debug版本上,測試內(nèi)購購買。

image
image

注意,單選編輯某個賬號,可以設(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舉例:

image

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的文檔怎么說:

image.png

注意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;
}

對上面代碼做一些說明:


image.png

因?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)該就基本能理解透徹了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容