一.總說內購的內容
1.協議、稅務和銀行業務 信息填寫
2.內購商品的添加
3.添加沙盒測試賬號
4.內購代碼的具體實現
5.內購的注意事項
二.協議、稅務和銀行業務 信息填寫
-
2.1、協議、稅務和銀行業務 信息填寫 的入口
-
2.2、選擇申請合同類型
-
進入協議、稅務和銀行業務頁面后,會有3種合同類型,如果你之前沒有主動申請過去合同,那么一般你現在激活的合同只有iOS Free Application一種。
-
頁面內容分為兩塊:
Request Contracts(申請合同)
Contracts In Effect(已生效合同)。
-
合同類型分為3種:
iOS Free Application(免費應用合同)
iOS Paid Application(付費應用合同)
iAd App NetNetwork(廣告合同)
這里我們主要主要講一下付費應用合同的申請流程。
-
2.3、申請iOS Paid Application合同(協議、稅務和銀行業務3個都要填寫)
2.4、Contact Info(填寫聯系方式)
如果你沒有添加過聯系人,你需要通過Add New Contact按鈕來添加一個新的聯系人。然后指定聯系人的職務,
職務如下:
Senior Management:高管
Financial:財務
Technical:技術支持
Legal:法務
Marketing:市場推廣
如果你是獨立開發者,可以全部填你自己一個人。
2.5、填寫銀行信息
選擇你的銀行賬戶,如果你沒有,點擊旁邊的Add Bank Account添加一個賬戶。下面是添加一個賬戶的流程。
-
2.5.1、選擇銀行所在的國家
-
2.5.2、填寫銀行標識CNAPS Code
如果你不知道CNAPS Code是多少,可以百度搜CNAPS Code來查詢,查詢時會根據3個關鍵信息來查詢,如下:
Bank Name:銀行的英文名稱(不能是拼音)
City:銀行所在的城市英文名稱(中國的城市用拼音)
Postal Code:郵編
然后在下面就會出來備選的銀行,選擇正確的銀行后,點擊next,進入下一步。
2.5.3、確認銀行信息
-
2.5.4、填寫銀行賬號信息
Bank Account Number:銀行賬號
Confirm Bank Account Number:再次輸入銀行賬號
Account Holder Name:持卡人姓名,中文名用拼寫,名在前,姓在后
Bank Account Currency:貨幣類型,一般國內的開發者選擇CNY
2.5.5、確認所有信息
-
2.6.填寫稅務信息(這個內容比較多)
-
2.6.1.稅務信息這一塊了解不是很多,不過因為是國內開發者,可以不用太費心,稅務信息分3種:
U.S Tax Forms:美國稅務
Australia Tax Forms:澳大利亞稅務
Canada Tax Forms:加拿大稅務
-
2.6.2.一堆條約
我選擇的是U.S Tax Forms,選擇后會問你兩個問題:
-
第1個問題如下:詢問你是否是美國居民,有沒有美國伙伴關系或者美國公司,如果沒有直接選擇No。
接下來第二個問題如下:詢問你有沒有在美國的商業性活動,沒有也直接選No
2.6.3.然后填寫你的稅務信息,包括以下幾點:
Individual or Organization Name:個人或者組織名稱
Country of incorporation: 所在國家
Type of Beneficial Owner:受益方式,獨立開發者選個人
Permanent Residence:居住地址
Mailing address:郵寄地址
Name of Person Making this Declaration:聲明人
Title:頭銜
2.6.4.打鈎
2.6.5.澳大利亞的不要管了
2.6.6.加拿大的也不用管了
2.7.填寫完成
-
2.8.待審核
你填寫完所有資料后,合同狀態就會變成Processing, 大概24小時內就會有結果。
三.內購商品的添加
3.1.創建內購商品
-
3.2.選擇內購類型
-
3.2.1.消耗型商品:類似游戲中的鑽石,還有現在某些APP中的貨幣,比如斗魚里的魚丸、映客里的映票。會被消耗的,要選擇消耗型商品
注意:大多數的消耗型商品都是需要登錄的,因為需要在數據庫存余額。
需要注意的是:在登錄之前,你最好不要讓用戶看到商品,有可能會因為這個原因被拒(大家都說看運氣)
-
3.2.2.非消耗型商品:無法被消耗的商品,比如上文提到的視頻課程,一次購買,就應該永久可以觀看
注意:當你使用非消耗型商品時,你需要添加一個恢復購買的按鈕
這個常見於各種游戲中,其實知道這個規定以后還是挺好理解的,非消耗型商品是不可被消耗的,一次購買終身使用的,非消耗型的商品是跟appleId綁定的,就是你平時下載APP讓你輸入賬號密碼的內個。
你需要一個恢復購買的按鈕,來讓用戶恢復他購買的內容
-
3.2.3.訂閱類型商品:如果你的公司是外包公司,有訂閱類型商品的APP一定要用客戶的賬號提交審核,因為當APP中有過訂閱類型商品,注意是有過,創建過再刪除也算,這個APP無法被轉移賬號
注意:使用或曾經使用過訂閱型商品的APP無法轉移
3.3.創建好的產品
3.4.在上線的時候記得添加內購的商品
四.添加沙盒測試賬號
-
4.1.添加沙盒測試的入口
4.2.添加沙盒測試賬號
4.3.具體的測試賬號信息填寫
五.內購代碼的具體實現(這才是大家所期望看到的)
-
5.1.我創建了一個購買金幣的內購控制器ApplePayCIOViewController
在此,我僅僅向大家貼出.m的詳細代碼
-
5.2.內購的流程詳細講解
-
5.2.1. 用戶先拿到購買產品的單子,
-
5.2.2.拿着單子去蘋果那里交錢,交完錢讓蘋果在單子上蓋個章
-
5.2.3.拿着蓋了章的單子傳給自己的服務器來驗證是否真的支付成功
-
5.2.4.根據服務器返回的信息做具體的處理
-
5.3.上代碼
-
5.3.1..先導入StoreKit.framework庫;
-
5.3.2.創建ApplePayCIOViewController,遵守協議<SKPaymentTransactionObserver,SKProductsRequestDelegate>
-
5.3.3.ApplePayCIOViewController.m代碼
#import "ApplePayCIOViewController.h" #import <StoreKit/StoreKit.h> // 產品的ID #define ProductID1 @"CIOCourses1" @interface ApplePayCIOViewController ()<SKProductsRequestDelegate,SKPaymentTransactionObserver> { NSString *selectProductID; } @end @implementation ApplePayCIOViewController -(void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; // 添加觀察者 [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; } -(void)viewWillDisappear:(BOOL)animated{ [super viewWillDisappear:animated]; // 移除觀察者 [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; } - (void)viewDidLoad { [super viewDidLoad]; self.title = @"內購"; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]initWithTitle:@"測試" style:UIBarButtonItemStylePlain target:self action:@selector(test)]; // 恢復購買的按鈕 UIButton * revert = [[UIButton alloc]initWithFrame:CGRectMake(20, 100, 100, 80)]; [revert setBackgroundColor:JKRandomColor]; [revert addTarget:self action:@selector(replyToBuy) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview: revert]; self.view.backgroundColor =[UIColor redColor]; } #pragma mark 恢復購買(主要是針對非消耗產品) -(void)replyToBuy{ [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; } #pragma mark 測試內購 -(void)test{ if([SKPaymentQueue canMakePayments]){ // productID就是你在創建購買項目時所填寫的產品ID selectProductID = [NSString stringWithFormat:@"%@",ProductID1]; [self requestProductID:selectProductID]; }else{ // NSLog(@"不允許程序內付費"); UIAlertView *alertError = [[UIAlertView alloc] initWithTitle:@"溫馨提示" message:@"請先開啟應用內付費購買功能。" delegate:nil cancelButtonTitle:@"確定" otherButtonTitles: nil]; [alertError show]; } } #pragma mark 1.請求所有的商品ID -(void)requestProductID:(NSString *)productID{ // 1.拿到所有可賣商品的ID數組 NSArray *productIDArray = [[NSArray alloc]initWithObjects:productID, nil]; NSSet *sets = [[NSSet alloc]initWithArray:productIDArray]; // 2.向蘋果發送請求,請求所有可買的商品 // 2.1.創建請求對象 SKProductsRequest *sKProductsRequest = [[SKProductsRequest alloc]initWithProductIdentifiers:sets]; // 2.2.設置代理(在代理方法里面獲取所有的可賣的商品) sKProductsRequest.delegate = self; // 2.3.開始請求 [sKProductsRequest start]; } #pragma mark 2.蘋果那邊的內購監聽 -(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{ NSLog(@"可賣商品的數量=%ld",response.products.count); NSArray *product = response.products; if([product count] == 0){ NSLog(@"沒有商品"); return; } for (SKProduct *sKProduct in product) { NSLog(@"pro info"); NSLog(@"SKProduct 描述信息:%@", sKProduct.description); NSLog(@"localizedTitle 產品標題:%@", sKProduct.localizedTitle); NSLog(@"localizedDescription 產品描述信息:%@",sKProduct.localizedDescription); NSLog(@"price 價格:%@",sKProduct.price); NSLog(@"productIdentifier Product id:%@",sKProduct.productIdentifier); if([sKProduct.productIdentifier isEqualToString: selectProductID]){ [self buyProduct:sKProduct]; break; }else{ //NSLog(@"不不不相同"); } } } #pragma mark 內購的代碼調用 -(void)buyProduct:(SKProduct *)product{ // 1.創建票據 SKPayment *skpayment = [SKPayment paymentWithProduct:product]; // 2.將票據加入到交易隊列 [[SKPaymentQueue defaultQueue] addPayment:skpayment]; // 3.添加觀察者,監聽用戶是否付錢成功(不在此處添加觀察者) //[[SKPaymentQueue defaultQueue] addTransactionObserver:self]; } #pragma mark 4.實現觀察者監聽付錢的代理方法,只要交易發生變化就會走下面的方法 -(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{ /* SKPaymentTransactionStatePurchasing, 正在購買 SKPaymentTransactionStatePurchased, 已經購買 SKPaymentTransactionStateFailed, 購買失敗 SKPaymentTransactionStateRestored, 回復購買中 SKPaymentTransactionStateDeferred 交易還在隊列里面,但最終狀態還沒有決定 */ for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchasing:{ NSLog(@"正在購買"); }break; case SKPaymentTransactionStatePurchased:{ NSLog(@"購買成功"); // 購買后告訴交易隊列,把這個成功的交易移除掉 [queue finishTransaction:transaction]; [self buyAppleStoreProductSucceedWithPaymentTransactionp:transaction]; }break; case SKPaymentTransactionStateFailed:{ NSLog(@"購買失敗"); // 購買失敗也要把這個交易移除掉 [queue finishTransaction:transaction]; }break; case SKPaymentTransactionStateRestored:{ NSLog(@"回復購買中,也叫做已經購買"); // 回復購買中也要把這個交易移除掉 [queue finishTransaction:transaction]; }break; case SKPaymentTransactionStateDeferred:{ NSLog(@"交易還在隊列里面,但最終狀態還沒有決定"); }break; default: break; } } } // 蘋果內購支付成功 - (void)buyAppleStoreProductSucceedWithPaymentTransactionp:(SKPaymentTransaction *)paymentTransactionp { NSString * productIdentifier = paymentTransactionp.payment.productIdentifier; // NSLog(@"productIdentifier Product id:%@", productIdentifier); NSString *transactionReceiptString= nil; //系統IOS7.0以上獲取支付驗證憑證的方式應該改變,切驗證返回的數據結構也不一樣了。 NSString *version = [UIDevice currentDevice].systemVersion; if([version intValue] >= 7.0){ // 驗證憑據,獲取到蘋果返回的交易憑據 // appStoreReceiptURL iOS7.0增加的,購買交易完成后,會將憑據存放在該地址 NSURLRequest * appstoreRequest = [NSURLRequest requestWithURL:[[NSBundle mainBundle]appStoreReceiptURL]]; NSError *error = nil; NSData * receiptData = [NSURLConnection sendSynchronousRequest:appstoreRequest returningResponse:nil error:&error]; transactionReceiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; }else{ NSData * receiptData = paymentTransactionp.transactionReceipt; // transactionReceiptString = [receiptData base64EncodedString]; transactionReceiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; } // 去驗證是否真正的支付成功了 [self checkAppStorePayResultWithBase64String:transactionReceiptString]; } - (void)checkAppStorePayResultWithBase64String:(NSString *)base64String { /* 生成訂單參數,注意沙盒測試賬號與線上正式蘋果賬號的驗證途徑不一樣,要給后台標明 */ /* 注意: 自己測試的時候使用的是沙盒購買(測試環境) App Store審核的時候也使用的是沙盒購買(測試環境) 上線以后就不是用的沙盒購買了(正式環境) 所以此時應該先驗證正式環境,在驗證測試環境 正式環境驗證成功,說明是線上用戶在使用 正式環境驗證不成功返回21007,說明是自己測試或者審核人員在測試 */ /* 蘋果AppStore線上的購買憑證地址是: https://buy.itunes.apple.com/verifyReceipt 測試地址是:https://sandbox.itunes.apple.com/verifyReceipt */ // NSNumber *sandbox; NSString *sandbox; #if (defined(APPSTORE_ASK_TO_BUY_IN_SANDBOX) && defined(DEBUG)) //sandbox = @(0); sandbox = @"0"; #else //sandbox = @(1); sandbox = @"1"; #endif NSMutableDictionary *prgam = [[NSMutableDictionary alloc] init];; [prgam setValue:sandbox forKey:@"sandbox"]; [prgam setValue:base64String forKey:@"reciept"]; /* 請求后台接口,服務器處驗證是否支付成功,依據返回結果做相應邏輯處理 0 代表沙盒 1代表 正式的內購 最后最驗證后的 */ /* 內購驗證憑據返回結果狀態碼說明 21000 App Store無法讀取你提供的JSON數據 21002 收據數據不符合格式 21003 收據無法被驗證 21004 你提供的共享密鑰和賬戶的共享密鑰不一致 21005 收據服務器當前不可用 21006 收據是有效的,但訂閱服務已經過期。當收到這個信息時,解碼后的收據信息也包含在返回內容中 21007 收據信息是測試用(sandbox),但卻被發送到產品環境中驗證 21008 收據信息是產品環境中使用,但卻被發送到測試環境中驗證 */ NSLog(@"字典==%@",prgam); } #pragma mark 客戶端驗證購買憑據 - (void)verifyTransactionResult { // 驗證憑據,獲取到蘋果返回的交易憑據 // appStoreReceiptURL iOS7.0增加的,購買交易完成后,會將憑據存放在該地址 NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; // 從沙盒中獲取到購買憑據 NSData *receipt = [NSData dataWithContentsOfURL:receiptURL]; // 傳輸的是BASE64編碼的字符串 /** BASE64 常用的編碼方案,通常用於數據傳輸,以及加密算法的基礎算法,傳輸過程中能夠保證數據傳輸的穩定性 BASE64是可以編碼和解碼的 */ NSDictionary *requestContents = @{ @"receipt-data": [receipt base64EncodedStringWithOptions:0] }; NSError *error; // 轉換為 JSON 格式 NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:&error]; // 不存在 if (!requestData) { /* ... Handle error ... */ } // 發送網絡POST請求,對購買憑據進行驗證 NSString *verifyUrlString; #if (defined(APPSTORE_ASK_TO_BUY_IN_SANDBOX) && defined(DEBUG)) verifyUrlString = @"https://sandbox.itunes.apple.com/verifyReceipt"; #else verifyUrlString = @"https://buy.itunes.apple.com/verifyReceipt"; #endif // 國內訪問蘋果服務器比較慢,timeoutInterval 需要長一點 NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:[[NSURL alloc] initWithString:verifyUrlString] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10.0f]; [storeRequest setHTTPMethod:@"POST"]; [storeRequest setHTTPBody:requestData]; // 在后台對列中提交驗證請求,並獲得官方的驗證JSON結果 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; [NSURLConnection sendAsynchronousRequest:storeRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { if (connectionError) { NSLog(@"鏈接失敗"); } else { NSError *error; NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; if (!jsonResponse) { NSLog(@"驗證失敗"); } // 比對 jsonResponse 中以下信息基本上可以保證數據安全 /* bundle_id application_version product_id transaction_id */ NSLog(@"驗證成功"); } }]; } @end
六.內購的注意事項
-
6.1.一般發生於首次提交app或添加新商品,當你的app通過審核以后,你發現在生產環境下獲取不到商品,這是因為app雖然過審核了,但是內購商品還沒有正式添加到蘋果的服務器里,耐心等待一段時間就可以啦~
-
6.2. 代碼中的_currentProId所填寫的是你的購買項目的的ID,這個和第二步創建的內購的productID要一致;本例中是 123。
-
6.3. 在監聽購買結果后,一定要調用[[SKPaymentQueue defaultQueue] finishTransaction:tran];來允許你從支付隊列中移除交易。
-
6.4. 沙盒環境測試appStore內購流程的時候,請使用沒越獄的設備。
-
6.5. 請務必使用真機來測試,一切以真機為准。
-
6.6. 項目的Bundle identifier需要與您申請AppID時填寫的bundleID一致,不然會無法請求到商品信息。
-
6.7. 真機測試的時候,一定要退出原來的賬號,才能用沙盒測試賬號
-
6.8. 二次驗證,請注意區分宏, 測試用沙盒驗證,App Store審核的時候也使用的是沙盒購買,所以驗證購買憑證的時候需要判斷返回Status Code決定是否去沙盒進行二次驗證,為了線上用戶的使用,驗證的順序肯定是先驗證正式環境,此時若返回值為21007,就需要去沙盒二次驗證,因為此購買的是在沙盒進行的。
-
6.9.您的應用是否處於等待開發者發布(Pending Developer Release)狀態?等待發布狀態的IAP是無法測試的。
-
6.10.您的內購項目是否是最近才新建的,或者進行了更改?內購項目需要一段時間才能反應到所有服務器上,這個過程一般是一兩小時,也可能再長一些達到若干小時。
-
6.11.您在iTC中Contracts, Tax, and Banking Information項目中是否有還沒有設置或者過期了的項目?不完整的財務信息無法進行內購測試。
-
6.12.您是在越獄設備上進行內購測試么?越獄設備不能用於正常內購,您需要重裝或者尋找一台沒有越獄的設備。
-
6.13.您的應用是否是被拒狀態(Rejected)或自己拒絕(Developer Rejected)了?被拒絕狀態的應用的話對應還未通過的內購項目也會一起被拒,因此您需要重新將IAP項目設為Cleared for Sale。
-
6.14.您使用的測試賬號是否是美國區賬號?雖然不是一定需要,但是鑒於其他地區的測試賬號經常抽風,加上美國區賬號一直很穩定,因此強烈建議使用美國區賬號。正常情況下IAP不需要進行信用卡綁定和其他信息填寫,如果你遇到了這種情況,可以試試刪除這個測試賬號再新建一個其他地區的。
-
6.15.您是否將設備上原來的app刪除了,並重新進行了安裝?記得在安裝前做一下Clean和Clean Build Folder。
-
6.16.您的plist中的Bundle identifier的內容是否和您的AppID一致?