iOS內(nèi)購(IAP)模塊總結(jié)(含漏單處理)

我也是第一次接觸iOS內(nèi)購示罗,其實(shí)一直以來我接觸的公司都是不愿意接入內(nèi)購的妖啥,畢竟要給蘋果分成埠居,所以也就沒有學(xué)習(xí)IAP流程編寫诞丽。最近公司考慮到虛擬會(huì)員防審核期間可以把iOS內(nèi)購作為備用方案,所以也就把iOS內(nèi)購流程做個(gè)梳理并根據(jù)咋們公司的業(yè)務(wù)特點(diǎn)寫了非續(xù)期訂購類型的IAP代碼拐格。
剛開始接觸內(nèi)購,也是千頭萬緒刑赶,所以現(xiàn)在網(wǎng)上找了幾篇IAP文章看了下捏浊,結(jié)合自己的整理,先總結(jié)了IAP大致流程撞叨。

前期準(zhǔn)備工作:

1. 閱讀蘋果的《App內(nèi)購買項(xiàng)目》文檔
2.去App Store connect稅務(wù)里面簽署內(nèi)購協(xié)議
登陸Apple開發(fā)官網(wǎng)金踪,選擇connect:

image.png

image.png

image.png

image.png

3.配置內(nèi)購項(xiàng)目,“App -->功能-->APP內(nèi)購買項(xiàng)目”
具體配置可以參考創(chuàng)建及發(fā)布說明
牵敷,下面是我的演示實(shí)例:
選擇“我的APP”

image.png

然后選擇你要接入內(nèi)購的APP胡岔,點(diǎn)擊進(jìn)入,選擇功能-APP內(nèi)購買項(xiàng)目
image.png

然后點(diǎn)擊+新增一個(gè)內(nèi)購商品枷餐,彈出下面對話框:
image.png

這里說明一下靶瘸,內(nèi)購產(chǎn)品分為4種熄赡,分別消耗浮声、非消耗、續(xù)期孟抗、非續(xù)期润匙,具體解釋整理如下:

蘋果的內(nèi)購分以下四類商品:
1诗眨、消耗型項(xiàng)目
只可使用一次的產(chǎn)品,使用之后即失效孕讳,必須再次購買匠楚。
示例:釣魚 App 中的魚食巍膘。
2、非消耗型項(xiàng)目
只需購買一次芋簿,不會(huì)過期或隨著使用而減少的產(chǎn)品峡懈。
示例:游戲 App 的賽道。
3益咬、自動(dòng)續(xù)期訂閱
允許用戶在固定時(shí)間段內(nèi)購買動(dòng)態(tài)內(nèi)容的產(chǎn)品逮诲。除非用戶選擇取消,否則此類訂閱會(huì)自動(dòng)續(xù)期幽告。
示例:每月訂閱提供流媒體服務(wù)的 App梅鹦。
4、非續(xù)期訂閱
允許用戶購買有時(shí)限性服務(wù)的產(chǎn)品冗锁。此 App 內(nèi)購買項(xiàng)目的內(nèi)容可以是靜態(tài)的齐唆。此類訂閱不會(huì)自動(dòng)續(xù)期。
示例:為期一年的已歸檔文章目錄訂閱冻河。

4.Xcode capablities 打開IAP開關(guān)

image.png

IAP內(nèi)購流程圖:

iOS內(nèi)購流程圖

代碼實(shí)現(xiàn):

1.判斷用戶是否具備支付權(quán)限

- (BOOL)canMakePurchase {
    if ([SKPaymentQueue canMakePayments]) {
        return YES;
    }else {
        //用戶未開啟內(nèi)購箍邮,彈框提示
        UIAlertView * alertView = [[UIAlertView alloc] initWithTitle:@"內(nèi)購未開啟" message:@"進(jìn)入“【設(shè)置】 - 開啟【屏幕使用時(shí)間】功能。然后在【屏幕使用時(shí)間】選項(xiàng)中選擇【內(nèi)容和隱私訪問限制】叨叙,選擇【iTunes Store 與 App store 購買】- 選擇【App內(nèi)購項(xiàng)目】- 選擇“允許”锭弊,將該功能開啟" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
        [alertView show];
        //內(nèi)購結(jié)束
        if (self.IAPurchaseResult) {
            self.IAPurchaseResult(IAPurchaseNotAllow);
        }
        return NO;
    }
}

2.獲取Apple內(nèi)購商品列表

- (void)fetchIAPProducts:(void(^)(void))block {
    if (!self.productResp) {
        //如果沒有商品信息,異步接口獲取
        Weakify(self);
        [self queryByPuoductId:IAProductID productInfoReuslts:^(SKProductsResponse * _Nonnull resp) {
            if (resp == nil) {
                if (weakself.IAPurchaseResult) {
                    weakself.IAPurchaseResult(IAPurchaseFailed);
                }
                return ;
            }
            if (resp.products.count == 0) {
                if (weakself.IAPurchaseResult) {
                    weakself.IAPurchaseResult(IAPurchaseNoProducts);
                }
                return ;
            }
            if (block) {
                block();
            }
        }];
    }else {
        if (block) {
            block();
        }
    }
}

//通過產(chǎn)品ID查詢商品信息
- (void)queryByPuoductId:(NSString *)productId
      productInfoReuslts:(void(^)(SKProductsResponse *resp))block {
    self.fetchProductBlock = block;
    NSSet *set = [NSSet setWithObject:productId];
    SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
    request.delegate = self;
    [request start];
}

#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(nonnull SKProductsRequest *)request didReceiveResponse:(nonnull SKProductsResponse *)response {
    self.productResp = response;
    if (self.fetchProductBlock) {
        self.fetchProductBlock(response);
    }
}

- (void)requestDidFinish:(SKRequest *)request {
    //do nothting
}

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
    self.productResp = nil;
    if (self.fetchProductBlock) {
        self.fetchProductBlock(nil);
    }
}

3.創(chuàng)建蘋果內(nèi)購支付

- (void)createInPurchasePay {
    SKProduct *product = nil;
    for (SKProduct *prod in self.productResp.products) {
        if ([prod.productIdentifier isEqualToString:IAProductID]) {
            product = prod;
            break;
        }
    }
    if (!product) {
        if (self.IAPurchaseResult) {
            self.IAPurchaseResult(IAPurchaseNoProducts);
        }
        return;
    }
    SKMutablePayment * payment = [SKMutablePayment paymentWithProduct:product];
    //透傳業(yè)務(wù)訂單
    payment.applicationUsername = self.orderId;
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

#pragma mark - 監(jiān)聽用戶支付交易變化
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchased://交易完成
                [self verifyReceiptByTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed://交易失敗
                [self failTransation:transaction];
                break;
            case SKPaymentTransactionStateRestored://已經(jīng)購買過該商品
                [self verifyReceiptByTransaction:transaction];
                break;
            case SKPaymentTransactionStatePurchasing:      //商品添加進(jìn)列表
                //解決applicationUsername支付一半kill進(jìn)程后為nil的問題
                [self saveCurrTransationBindedOrderId];
                break;
            default:
                break;
        }
    }
}

//交易失敗
- (void)failTransation:(SKPaymentTransaction *)transaction {
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    IAPurchaseStatus status = IAPurchaseFailed;
    if (transaction.error.code != SKErrorPaymentCancelled) {
        status = IAPurchaseCancel;
    }
    if (self.IAPurchaseResult) {
        self.IAPurchaseResult(status);
    }
}

//持久化當(dāng)前正在交易綁定的業(yè)務(wù)訂單
- (void)saveCurrTransationBindedOrderId {
    NSLog(@"商品添加進(jìn)列表");
    if (self.orderId) {
        NSDictionary *orderdic = @{@"productId":IAProductID,
                                   @"orderId": self.orderId
                                   };
        [[NSUserDefaults standardUserDefaults] setObject:orderdic forKey:@"persient.IAP.order"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }
}

- (NSString *)bindedOrderId {
    NSDictionary *dic = [[NSUserDefaults standardUserDefaults] objectForKey:@"persient.IAP.order"];
    if (dic) {
        return dic[@"orderId"];
    }else {
        return nil;
    }
}

4.驗(yàn)證票據(jù)

- (void)verifyReceiptByTransaction:(SKPaymentTransaction *)transaction {
    NSString *receiptString = [self iapReceipt];
    if (!receiptString) {
        if (self.IAPurchaseResult) {
            self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
        }
        return;
    }
    NSError *error;
    NSDictionary *requestContents = @{@"receipt-data": receiptString};
    NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                          options:0
                                                            error:&error];
    
    if (!requestData) { // 交易憑證為空驗(yàn)證失敗
        if (self.IAPurchaseResult) {
            self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
        }
        return;
    }
    //向蘋果服務(wù)器驗(yàn)證支付憑據(jù)真實(shí)性
    [self verifyRequestData:requestData testSandbox:NO transaction:transaction];
}

//獲取內(nèi)購票據(jù)
- (NSString *)iapReceipt {
    NSString *receiptString = nil;
    NSURL *rereceiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receipt = [NSData dataWithContentsOfURL:rereceiptURL];
    receiptString = [receipt base64EncodedStringWithOptions:0];
    return receiptString;
}

- (void)verifyRequestData:(NSData *)postData
                      testSandbox:(BOOL)test
              transaction:(SKPaymentTransaction *)transaction
{
    NSString *url = @"https://buy.itunes.apple.com/verifyReceipt";
    if (test) {
        url = @"https://sandbox.itunes.apple.com/verifyReceipt";
    }
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
    request.HTTPBody = postData;
    static NSString *requestMethod = @"POST";
    request.HTTPMethod = requestMethod;
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [NSURLConnection sendAsynchronousRequest:request queue:queue
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
                               if (connectionError) {
                                   // 無法連接服務(wù)器,購買校驗(yàn)失敗
                                   if (self.IAPurchaseResult) {
                                       self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
                                   }
                               } else {
                                   NSError *error;
                                   NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
                                   if (!jsonResponse) {
                                       // 蘋果服務(wù)器校驗(yàn)數(shù)據(jù)返回為空校驗(yàn)失敗
                                       if (self.IAPurchaseResult) {
                                           self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
                                       }
                                       [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                                       return ;
                                   }
                                   //先驗(yàn)證正式服務(wù)器,如果正式服務(wù)器返回21007再去蘋果測試服務(wù)器驗(yàn)證,沙盒測試環(huán)境蘋果用的是測試服務(wù)器
                                   NSString *status = [NSString stringWithFormat:@"%@", jsonResponse[@"status"]];
                                   if (status && [status isEqualToString:@"21007"]) {
                                       [self verifyRequestData:postData testSandbox:YES transaction:transaction];
                                   } else if (status && [status isEqualToString:@"0"]) {
                                       //訂單校驗(yàn)成功,給蝸蝸生活訂單會(huì)員充值
                                       NSString *orderId = transaction.payment.applicationUsername;
                                       if (!orderId) {
                                           orderId = [self bindedOrderId];
                                       }
                                       [self chargeWowoVipOrderId:orderId];
                                       [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                                   }else {
                                       // 蘋果服務(wù)器校驗(yàn)數(shù)據(jù)返回為空校驗(yàn)失敗
                                       if (self.IAPurchaseResult) {
                                           self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
                                       }
                                       [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                                   }
                               }
                           }];
    
}

5.最后一步擂错,給會(huì)員訂單沖會(huì)員

- (void)chargeWowoVipOrderId:(NSString *)orderId {
    if (!orderId.length) {
        if (self.IAPurchaseResult) {
            self.IAPurchaseResult(IAPurchaseVerifyReciptSuccess);
        }
        return;
    }
    //開始調(diào)用充值接口...
    if (self.IAPurchaseResult) {
        self.IAPurchaseResult(IAPurchaseSuccess);
    }
}

關(guān)于漏單的問題

由于用戶可能在支付過程中中途網(wǎng)絡(luò)不佳味滞,或者程序突然crash的情況下,有可能用戶支付成功了钮呀,但是驗(yàn)證票據(jù)等后續(xù)操作沒有走完剑鞍,也就沒有給用戶實(shí)際充值的情況。
這種情況下爽醋,我們可以將payment監(jiān)聽放到APP啟動(dòng)里啟用全局監(jiān)聽蚁署,那么下次APP啟動(dòng)后,會(huì)重新走支付交易事務(wù)變化的監(jiān)聽蚂四,就可以繼續(xù)完成票據(jù)驗(yàn)證以及給用戶充值的操作光戈。
我們就可以將內(nèi)購類設(shè)計(jì)成單例模式,在init時(shí)候即添加全局通知遂赠,然后實(shí)現(xiàn)SKPaymentTransactionObserver委托:

//單例模式
+ (instancetype)shared {
    dispatch_once(&onceToken, ^{
        sharedInstance = [[InAppPurchaseManager alloc] init];
    });
    return sharedInstance;
}

- (instancetype)init {
    if (self = [super init]) {
        //添加支付交易的全局Observer
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
    return self;
}

然后我們再appDlegate的啟動(dòng)方法里調(diào)用如下方法:

/**
 內(nèi)購準(zhǔn)備環(huán)境(在appDelegateAPP每次啟動(dòng)時(shí)調(diào)用)
 */
- (void)parpareIAP {
    [self queryByPuoductId:IAProductID productInfoReuslts:^(SKProductsResponse * _Nonnull resp) {
        
    }];
}

這樣appDelegate會(huì)自動(dòng)添加了全局事務(wù)觀察了田度。

關(guān)于applicationUsername為nil的問題

我們將我們自己的業(yè)務(wù)訂單ID跟Apple的支付事務(wù)是通過applicationUsername這個(gè)屬性關(guān)聯(lián)的。但是蘋果并不幫我們將這個(gè)屬性做了持久化操作解愤,只在內(nèi)存中镇饺。
復(fù)現(xiàn)場景:當(dāng)用戶殺掉APP后,重新打開APP后送讲,上次的訂單ID透傳給applicationUsername=nil,也就是訂單ID丟失了奸笤,那么后續(xù)給用戶充值的重要入?yún)⒂唵蜪D沒有了惋啃,也就無法充值。

解決方案:

這個(gè)解決方案监右,我是參考了這篇作者提供的思路:貝聊 IAP 實(shí)戰(zhàn)之訂單綁定边灭,粗放性訂單持久化。

思路

上述思路實(shí)現(xiàn):
粗放型持久化思路

//持久化當(dāng)前正在交易綁定的業(yè)務(wù)訂單
- (void)saveCurrTransationBindedOrderId {
    NSLog(@"商品添加進(jìn)列表");
    if (self.orderId) {
        NSDictionary *orderdic = @{@"productId":IAProductID,
                                   @"orderId": self.orderId
                                   };
        [[NSUserDefaults standardUserDefaults] setObject:orderdic forKey:@"persient.IAP.order"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }
}
獲取持久化訂單

以上健盒,內(nèi)購IAP完結(jié)绒瘦!

關(guān)于網(wǎng)上很多IAP文章做了大量支付票據(jù)本地存儲(chǔ)操作,我覺得完全沒有必要扣癣,其實(shí)蘋果內(nèi)購的每次交易事務(wù)都做了本地持久化了惰帽,所有的上次沒有完成的事務(wù),都可以在下次交易變化中一次性再提交上去父虑「眯铮客戶端沒必要再存了。所以士嚎,希望大家不要被一些本地存儲(chǔ)事務(wù)票據(jù)等給誤導(dǎo)了呜魄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市莱衩,隨后出現(xiàn)的幾起案子爵嗅,更是在濱河造成了極大的恐慌,老刑警劉巖笨蚁,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件操骡,死亡現(xiàn)場離奇詭異,居然都是意外死亡赚窃,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進(jìn)店門岔激,熙熙樓的掌柜王于貴愁眉苦臉地迎上來勒极,“玉大人,你說我怎么就攤上這事虑鼎∪枘洌” “怎么了?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵炫彩,是天一觀的道長匾七。 經(jīng)常有香客問我,道長江兢,這世上最難降的妖魔是什么昨忆? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮杉允,結(jié)果婚禮上邑贴,老公的妹妹穿的比我還像新娘席里。我一直安慰自己,他們只是感情好拢驾,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布奖磁。 她就那樣靜靜地躺著,像睡著了一般繁疤。 火紅的嫁衣襯著肌膚如雪咖为。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天稠腊,我揣著相機(jī)與錄音躁染,去河邊找鬼。 笑死麻养,一個(gè)胖子當(dāng)著我的面吹牛褐啡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鳖昌,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼备畦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了许昨?” 一聲冷哼從身側(cè)響起懂盐,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎糕档,沒想到半個(gè)月后莉恼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡速那,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年俐银,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片端仰。...
    茶點(diǎn)故事閱讀 40,615評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡捶惜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出荔烧,到底是詐尸還是另有隱情吱七,我是刑警寧澤,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布鹤竭,位于F島的核電站踊餐,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏臀稚。R本人自食惡果不足惜吝岭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧苍碟,春花似錦酒觅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蜓肆,卻和暖如春颜凯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背仗扬。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工症概, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人早芭。 一個(gè)月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓彼城,卻偏偏與公主長得像,于是被迫代替她去往敵國和親退个。 傳聞我的和親對象是個(gè)殘疾皇子募壕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內(nèi)容