蘋果內購流程及后臺配置

第一部分:在Apple后臺添加一個內購產品

1、登錄appStoreConnect,如下圖所示,添加一個商品

增加內購.png
IAP類型類型主要有4種:

1、Consumable products 適用于可多次購買的消耗型項目,如游戲道具、虛擬幣等。

2、Non-consumable products 適用于一次購買永久有效的項目,如電子書、游戲關卡等。該類型項目支持跨設備同步和本地restore,比如說,用戶在某個App中購買了一本書,可在所有相同Apple ID設備的App中免費獲取這本書。

3、Auto-renewable subscriptions 適用于自動續費的訂閱項目,如Apple Music的按月訂閱,用戶購買后會每月自動續費,直到用戶手動取消或者開發者下架IAP項目。

4、Non-renewable subscriptions 適用于固定有效期的非自動續費項目,如云音樂的會員和一些視頻App的會員。

由于我們是充值虛擬幣學點,所以選擇了Consumable類型。

2、填寫商品名稱、Product ID(Product ID一旦創建不可修改),選擇價格,然后拉到最下面添加商品購買時的截圖,最后保存

創建一個學點.jpeg
點擊④,會進入價格列表,對照下圖中的價格,選擇商品想要賣的價格在③中進行選擇
價格參考.jpg

3、記住要添加截圖,不然狀態會變成Miss Metadata,添加截圖數據沒有問題后會變成Ready to Submit狀態

MissMetaData.jpeg

4、在App提交審核時,把App當前版本用到的內購商品添加到App中,不添加的話,蘋果審核會被拒絕,報你有一個或多個內購商品沒有提交審核的Issue

添加內購商品.png

第二部分:蘋果支付流程

支付流程簡單來講就是在App中點擊購買商品按鈕的時候,把在Apple后臺設置的Product ID通過SKProductsRequest傳給Apple后臺,Apple后臺會把商品對象SKProduct回調給App,然后再把SKProduct加到支付隊列中,這個時候就會有彈框顯示支付金額讓你輸密碼了,支付成功后蘋果會把交易憑證返回,拿著憑證去公司服務器驗證,驗證為有效憑證發學點,交易完成

1.點擊購買商品按鈕時傳入在Apple后臺的In-App Purchases中設置的相應的Product ID
2、傳入Product ID參數,通過SKProductsRequest發起請求,監聽回調結果
-(void)starBuyToAppStoreWithGoodsId:(NSString *)goodsID cannotPayment:(void(^)(void))cannotPayment{
    //判斷app是否允許apple支付
    if (![SKPaymentQueue canMakePayments]) {
        if (cannotPayment) {
            cannotPayment();
        }
        return;
    }
    //1.點擊購買商品時傳入在Apple后臺的In-App Purchases中設置的相應的Product ID
    //goodsID 就是在蘋果后臺設置的商品ID
    self.goodsId = goodsID; //比如Product ID可以是 com.example.example_LevelA
    NSArray *product = [[NSArray alloc] initWithObjects:goodsID,nil];
    NSSet *nsset = [NSSet setWithArray:product];
    
    //2、傳入Product ID參數,通過SKProductsRequest發起請求,監聽回調結果
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
    request.delegate = self;
    [request start];
}
3、在需要監聽商品請求回調的地方,實現SKProductsRequestDelegate,確保Apple后臺返回的Product ID與步驟2中請求的一樣
4、把SKProduct加到支付隊列之前,創建一個訂單持久化到本地,用戶可以查看訂單狀態,在支付流程中會遇到各種情況導致支付失敗,至少可以列舉5種可能的狀態:0=待充值,1=充值完成,2=充值中,3=充值取消,4=充值失敗,可以根據各種狀態去更新訂單狀態
5、把SKProduct加入到支付隊列中,并給SKMutablePayment對象添加唯一標識用于交易結束后獲取相應的訂單改變訂單狀態,當被成功添加到支付隊列后這個時候就會有彈框了
#pragma mark -SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    NSArray *products = response.products;
    
    if([products count] == 0){
        if (self.delegate && [self.delegate respondsToSelector:@selector(appStorePayFailed)]) {
            [self.delegate appStorePayFailed];
        }
        return;
    }
    
    //3、在需要監聽商品請求回調的地方,實現SKProductsRequestDelegate,確保Apple后臺返回的Product ID與步驟2中請求的一樣
    SKProduct *requestProduct = nil;
    for (SKProduct *product in products) {
        if([product.productIdentifier isEqualToString:self.goodsId]){
            requestProduct = product;
            break;
        }
    }
    
    if (!requestProduct) {
        return;
    }
    
//4、把SKProduct加到支付隊列之前,創建一個訂單持久化到本地,用戶可以查看訂單狀態,在支付流程中會遇到各種情況導致支付失敗,至少可以列舉5種可能的狀態:0=待充值,1=充值完成,2=充值中,3=充值取消,4=充值失敗,可以根據各種狀態去更新訂單狀態
    NSString *startTime = [NSString stringWithFormat:@"%.0f", [NSDate date].timeIntervalSince1970];
    NSString *applicationUsername = [NSString stringWithFormat:@"%@/%@/%@",[ZGAppInfoUtil appID], startTime, requestProduct.price];
    NSDictionary *dict = @{
        @"price" : requestProduct.price,
        @"startTime" : startTime,
        @"status" : @(InAppPurchaseStatusPrepare),
        @"receiptData" : @"",
        @"transactionID" : @"",
        @"applicationUserName" : applicationUsername
    };
    [self.chargeManager makeLearnPointOrderWithInfo:dict];
    
    
    //5、把SKProduct加入到支付隊列中,并給SKMutablePayment對象添加唯一標識用于交易結束后獲取相應的訂單改變訂單狀態,當被成功添加到支付隊列后這個時候就會有彈框了
    SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:requestProduct];
    payment.applicationUsername = applicationUsername;
    [[SKPaymentQueue defaultQueue] addPayment:payment];//將票據加入到交易隊列
}
6、監聽購買結果 - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions,根據不同的交易狀態去處理相應的邏輯
#pragma mark -SKPaymentTransactionObserver
//監聽購買結果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
     //6、支付結果的回調,根據不同的交易狀態去處理相應的邏輯
    
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing:
                NSLog(@"交易已經添加到服務隊列中");
                break;
            case SKPaymentTransactionStatePurchased:
                NSLog(@"已經付費了,交易完成");
                [self completeTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                [self failedTransaction:transaction];
                NSLog(@"交易被取消或者添加到交易隊列失敗");
                break;
            case SKPaymentTransactionStateRestored:
                NSLog(@"交易從購買歷史列表中被恢復,客戶端應該完成交易");
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
            case SKPaymentTransactionStateDeferred:
                NSLog(@"在交易隊列中,還沒有最終的結果");
                break;
            default:
                break;
        }
    }
}
7、當交易狀態為SKPaymentTransactionStatePurchased,意味著支付完成了,從沙盒中獲取交易憑證
8、拿到交易憑證后,在去后臺服務器后臺驗證之前,需要把訂單狀態改為充值中,在持久化的訂單列表中對狀態為充值中的訂單可以發起補充值請求
9、用戶蘋果支付完成了,需要拿著蘋果返回的憑證去服務器校驗,校驗成功發放學點,并且該條訂單設置為完成狀態,可能因為網絡原因校驗訂單失敗,該條訂單狀態保持充值中狀態
//支付完成
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
    if (transaction.payment.productIdentifier && transaction.transactionIdentifier) {
        // 7、從沙盒中拿到交易憑證
        NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
        NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
        if (!receiptData) {
            return;
        }
        NSString *receiptString = [receiptData base64EncodedStringWithOptions:0];
        if (!receiptData) {
            return;
        }
        
        //8、拿到交易憑證后,在去后臺服務器后臺驗證之前,需要把訂單狀態改為充值中,在持久化的訂單列表中對狀態為充值中的訂單可以發起補充值請求
        NSMutableDictionary *orderDic = [self.chargeManager getLearnPointOrderWithApplicationUsername:transaction.payment.applicationUsername].mutableCopy;
        [orderDic setObject:@(InAppPurchaseStatusOngoing) forKey:@"status"];
        [orderDic setObject:receiptString forKey:@"receiptData"];
        [orderDic setObject:transaction.transactionIdentifier forKey:@"transactionID"];
        
        //9、用戶蘋果支付完成了,需要拿著蘋果返回的憑證去服務器校驗,校驗成功發放學點,并且該條訂單設置為完成狀態,可能因為網絡原因校驗訂單失敗,該條訂單狀態保持充值中狀態
        NSMutableDictionary *params = [NSMutableDictionary dictionary];
        params[@"apple_receipt"] = receiptString ?: @"";
        __weak typeof(self) weakSelf = self;
        [OrderChargeManager verifyToServerWithReceipt:params success:^(NSDictionary * _Nonnull result) {
            if ([result[@"flag"] integerValue] == 1) { //校驗成功
                if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(appStoreDidPaySuccess)]) {
                    [weakSelf.delegate appStoreDidPaySuccess];
                }
                [orderDic setObject:@(InAppPurchaseStatusCompleted) forKey:@"status"];
            } else {
                if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(appStoreWillPaySuccess)]) {
                    [weakSelf.delegate appStoreWillPaySuccess];
                }
            }
            
            [self.chargeManager makeLearnPointOrderWithInfo:orderDic];
        } fail:^(NSError * _Nonnull error) {
            if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(appStoreWillPaySuccessNetError)]) {
                [weakSelf.delegate appStoreWillPaySuccessNetError];
            }
            [weakSelf.chargeManager makeLearnPointOrderWithInfo:orderDic];
        }];
    }
    
    //不管憑證驗證結果,最后完成交易
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
10、如果交易失敗,把訂單狀態更新為相應的狀態
//交易失敗
- (void)failedTransaction:(SKPaymentTransaction *)transaction {
   // 10、如果交易失敗,把訂單狀態更新為相應的狀態
    NSMutableDictionary *orderDic = [self.chargeManager getLearnPointOrderWithApplicationUsername:transaction.payment.applicationUsername].mutableCopy;
    if (transaction.error.code == SKErrorPaymentCancelled) {
        if (self.delegate && [self.delegate respondsToSelector:@selector(appStorePayCancel)]) {
            [self.delegate appStorePayCancel];
        }
        [orderDic setObject:@(InAppPurchaseStatusCanceled) forKey:@"status"];
    } else {//其他錯誤
        if (self.delegate && [self.delegate respondsToSelector:@selector(appStorePayFailed)]) {
            [self.delegate appStorePayFailed];
        }
        [orderDic setObject:@(InAppPurchaseStatusFailed) forKey:@"status"];
    }
    
    [self.chargeManager makeLearnPointOrderWithInfo:orderDic];
    
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

第三部分:訂單狀態記錄

在發起支付的過程中會遇到各種各樣的問題,比如用戶中途取消、用戶支付完成后拿著交易憑證去公司服務器驗證的時候網絡不好或者公司服務器掛了,為了防止掉單也讓用戶看到自己訂單的支付記錄,有必要在本地使用Plist持久化一個訂單列表

1、在Document目錄下創建一個Plist文件
- (NSString *)filePath {
    if (!_filePath) {
        NSString *document = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
        NSString *path = [document stringByAppendingPathComponent:@"PointCoinOrder.plist"];
        _filePath = path;
    }
    return _filePath;
}
2、創建訂單,更新訂單 (因為一條訂單信息在支付流程的不同階段,最終的狀態會發生改變(創建、取消、失敗、完成等),需要要把老的訂單刪除,更新訂單狀態信息之后的訂單追加進去)
- (dispatch_queue_t)queue {
    if (!_queue) {
        _queue = dispatch_queue_create("com.example.xxxxx", DISPATCH_QUEUE_SERIAL);
    }
    return _queue;
}

- (void)makeLearnPointOrderWithInfo:(NSDictionary *)dict {
    dispatch_async(self.queue, ^{
        NSMutableArray *resultArray = [NSMutableArray arrayWithArray:[NSArray arrayWithContentsOfFile:self.filePath]];
        //因為一條訂單信息在支付流程的不同階段,最終的狀態會發生改變(創建、取消、失敗、完成等),需要要把老的訂單刪除,更新狀態信息之后的訂單追加進去
        for (NSDictionary *subDict in resultArray) {
            if ([[subDict objectForKey:@"applicationUserName"] isEqualToString:[dict objectForKey:@"applicationUserName"]]) {
                [resultArray removeObject:subDict];
                break;
            }
        }
        if (dict) {
            [resultArray addObject:dict];
        }
        [resultArray writeToFile:self.filePath atomically:YES];
    });
}

//根據訂單的ID獲取訂單
- (NSDictionary *)getLearnPointOrderWithApplicationUsername:(NSString *)applicationUsername {
    NSMutableArray *resultArray = [NSMutableArray arrayWithArray:[NSArray arrayWithContentsOfFile:self.filePath]];
    NSMutableDictionary *resultDict;
    for (NSDictionary *subDict in resultArray) {
        if ([[subDict objectForKey:@"applicationUserName"] isEqualToString:applicationUsername]) {
            resultDict = [NSMutableDictionary dictionaryWithDictionary:subDict];
            //移除舊的字典
            [resultArray removeObject:subDict];
            break;
        }
    }
    return resultDict;

}
3、去公司服務器驗證訂單
+ (void)verifyToServerWithReceipt:(NSDictionary *)receiptInfo success:(void(^)(NSDictionary *result))success fail:(void(^)(NSError *error))fail {
    
}

參考資料:

iOS 支付 --蘋果內購解讀

iOS內購全面實戰

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容