IAP 漏單相關(guān)

漏單的原因

蘋果應(yīng)用內(nèi)支付漏單的原因有很多,例如:驗證階段钞它,app 閃退彭沼、app 斷網(wǎng)或者網(wǎng)絡(luò)不好、app 登錄超時
根本原因在于蘋果只負責收錢画侣,而收錢之后的校驗工作交給開發(fā)者自己來處理冰悠,開發(fā)者拿到憑證 receipt 后,去請求自己的服務(wù)器驗證棉钧。

處理漏單的過程

  1. 用戶已經(jīng)付費屿脐,此時提示購買完成
  2. 開發(fā)者拿著獲取的憑證 receipt 去請求自己的服務(wù)器,并且客戶端記錄并且本地記錄這條 keytransactionID 的交易相關(guān)信息(包括訂單號 paymentID , 交易號 transactionID )宪卿,另外憑證receipt 最好不要保存在客戶端
  3. 服務(wù)器拿到客戶端傳過來的交易相關(guān)信息后的诵,首先校驗訂單號是否正確,是否與用戶相匹配佑钾,避免用戶切換賬號的問題西疤。校驗訂單號通過后再去蘋果服務(wù)器校驗憑證,這里需要注意休溶,在蘋果審核的時候是走沙盒環(huán)境的代赁,生產(chǎn)走正式環(huán)境,服務(wù)端需要先正常流程驗證正式環(huán)境兽掰,當返回錯誤碼顯示環(huán)境錯誤的時候芭碍,走測試環(huán)境
  4. 正常情況下,校驗憑證通過孽尽,客戶端 finish 掉 這個 transaction窖壕,并且本地清除掉這條keytransactionID對應(yīng)的交易信息,但是杉女,當在請求服務(wù)端的時候瞻讽,出現(xiàn)程序閃退、網(wǎng)絡(luò)不好的時候熏挎,就校驗失敗了速勇,但是用戶的錢蘋果已經(jīng)扣掉,服務(wù)沒有開通的現(xiàn)象坎拐,也就是漏單了烦磁,如果這時用戶再次點擊這個productID對應(yīng)的商品,就會報此應(yīng)用將免費恢復(fù)的提示廉白,這里只能在用戶下一次打開應(yīng)用的時候處理漏單流程了
  5. 當用戶再次啟動應(yīng)用程序的時候个初,當有未finish 掉的 transaction 時,應(yīng)用程序會自動調(diào)用 方法 paymentQueue: updatedTransactions:
  6. 那我們通過什么來判斷這次校驗的是漏單流程還是正常流程呢猴蹂?
    布爾值 isCash 是判斷漏單流程的條件院溺,如果用戶是點擊購買按鈕進來的,isCash為YES磅轻, 那我們可以判斷走正常流程珍逸,如果 isCash 為NO逐虚,我們就走漏單流程,本地讀取之前保存的交易信息谆膳,再次請求服務(wù)端叭爱,校驗憑證

代碼

  1. .h 文件
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>

@interface LxInAppPurchaseManager : NSObject
typedef void (^LxInAppPurchaseProductCompleteSuccessBlock)(id data);
typedef void (^LxInAppPurchaseProductCompleteFailureBlock)(id data);
/**
 訂單號,服務(wù)器生成的交易編號
 */
@property(nonatomic,copy)NSString *paymentID;

/**
 是否為漏單漱病,如果為真走正常流程买雾,否則走漏單流程
 */
@property (nonatomic, assign)BOOL cash;

/**
 單例類

 @return 單例對象
 */
+ (instancetype)sharedManager;

/**
 點擊某項內(nèi)購商品的購買按鈕

 @param productID  iTunes Connect 上商品的標識符
 */
- (void)payBtnPressed:(NSString *)productID completeSuccessBlock:(LxInAppPurchaseProductCompleteSuccessBlock)__successBlock
         failureBlock:(LxInAppPurchaseProductCompleteFailureBlock)__failureBlock;

/**
 開始監(jiān)聽
 */
- (void)startObserver;


/**
 停止監(jiān)聽
 */
- (void)stopObserver;

@end
  1. .m文件
#import "LxInAppPurchaseManager.h"
#import "SignServiceVerifyReceiptTask.h"
#import "LxIapRequestContent.h"
#import "LxArchiverManager.h"
@interface LxInAppPurchaseManager()<SKProductsRequestDelegate, SKPaymentTransactionObserver>
@property (nonatomic, assign)BOOL isObserver; // 程序是否添加監(jiān)聽
@property(nonatomic,copy)NSString *productID; // iTunes Connect上商品的標識符
@property (nonatomic, copy)NSString *transactionID;
@property (nonatomic, strong)SKProduct *product;

@property (nonatomic, copy)LxInAppPurchaseProductCompleteSuccessBlock successBlock;
@property (nonatomic, copy)LxInAppPurchaseProductCompleteFailureBlock failureBlock;
@end

@implementation LxInAppPurchaseManager
static LxInAppPurchaseManager *purchaseManager = nil;
+ (instancetype)sharedManager{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        purchaseManager = [[LxInAppPurchaseManager alloc] init];
    });
    return purchaseManager;
}

- (void)payBtnPressed:(NSString *)productID completeSuccessBlock:(LxInAppPurchaseProductCompleteSuccessBlock)__successBlock
         failureBlock:(LxInAppPurchaseProductCompleteFailureBlock)__failureBlock{
    self.successBlock = __successBlock;
    self.failureBlock = __failureBlock;

    self.productID = productID;
    if (!self.productID) {
        [MBProgressHUD wyp_error:@"商品標識符不能為空" block:nil];
        return;
    }

    // 判斷用戶是否允許應(yīng)用內(nèi)付費
    if ([SKPaymentQueue canMakePayments]){
        SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:productID]];
        request.delegate = self;
        [request start];
    }else
        [MBProgressHUD wyp_error:@"用戶不允許內(nèi)購" block:nil];
}

#pragma mark - 監(jiān)聽內(nèi)購

// 開始監(jiān)聽內(nèi)購
- (void)startObserver {
    // isObserver 程序是否添加監(jiān)聽
    if (!self.isObserver) {
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
        NSLog(@"http://----------------------------- 開始監(jiān)聽內(nèi)購 ------------------------------//");
        self.isObserver = YES;
    }
}

// 移除監(jiān)聽內(nèi)購
- (void)stopObserver {
    if (self.isObserver) {
        [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
        NSLog(@"http://----------------------------- 移除監(jiān)聽內(nèi)購 ------------------------------//");
        self.isObserver = NO;
    }
}

#pragma mark - SKProductsRequestDelegate

// 檢索成功后的回調(diào),從 App Store 獲取產(chǎn)品列表信息請求的響應(yīng)
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    NSArray *products = response.products;
    if (products.count != 0) {
        for (SKProduct *_product in products) {
            if ([_product.productIdentifier isEqualToString:self.productID]) {
                self.product = _product;
            }
        }
        SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:self.product];
        // 服務(wù)器的交易編號杨帽,也就是訂單號 paymentID
        payment.applicationUsername = self.paymentID;
        // 發(fā)起購買
        NSLog(@"http://----------------------------- 發(fā)起購買 ------------------------------//");
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }else{
        [MBProgressHUD wyp_error:@"無法獲取商品" block:nil];
    }
}

#pragma mark - SKRequestDelegate

// 檢索失敗后的回調(diào)
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
    [MBProgressHUD wyp_error:@"請求蘋果服務(wù)器失敗" block:nil];
}

// 檢索結(jié)束
- (void)requestDidFinish:(SKRequest *)request{
    NSLog(@"http://----------------------------- 蘋果反饋信息結(jié)束 ------------------------------//");
}

#pragma mark -

// 當用戶購買的操作有結(jié)果時漓穿,就會觸發(fā)下面的回調(diào)函數(shù)
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
    for (SKPaymentTransaction *transaction in transactions) {
        self.transactionID = transaction.transactionIdentifier;
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchased: // 交易成功
                [self purchasedTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed: // 交易失敗
                [self failedTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored: // 已經(jīng)購買過該商品,消耗型不支持恢復(fù),TODU
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
            case SKPaymentTransactionStatePurchasing: // 已經(jīng)在商品列表中
                NSLog(@"http://----------------------------- 等待支付 ------------------------------//");
                break;
            case SKPaymentTransactionStateDeferred: // 最終狀態(tài)未確定
                NSLog(@"http://----------------------------- 最終狀態(tài)未確定 ------------------------------//");
                break;
            default:
                break;
        }
    }
}

// 恢復(fù)購買
- (void)restoredTransation:(SKPaymentTransaction *)transaction{
    [MBProgressHUD wyp_error:@"" block:^{
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    }];
}

// 付款失敗后注盈,結(jié)束交易
- (void)failedTransaction:(SKPaymentTransaction *)transaction{
    NSString *detail = @"";
    switch (transaction.error.code) {
        case SKErrorUnknown:
            NSLog(@"SKErrorUnknown");
            detail = @"未知的錯誤";
            break;

        case SKErrorClientInvalid:
            NSLog(@"SKErrorClientInvalid");
            detail = @"當前蘋果賬戶無法購買商品(如有疑問晃危,可以詢問蘋果客服)";
            break;

        case SKErrorPaymentCancelled:
            NSLog(@"SKErrorPaymentCancelled");
            detail = @"購買已取消";
            break;

        case SKErrorPaymentInvalid:
            NSLog(@"SKErrorPaymentInvalid");
            detail = @"購買無效(如有疑問,可以詢問蘋果客服)";
            break;

        case SKErrorPaymentNotAllowed:
            NSLog(@"SKErrorPaymentNotAllowed");
            detail = @"當前蘋果設(shè)備無法購買商品(如有疑問老客,可以詢問蘋果客服)";
            break;

        case SKErrorStoreProductNotAvailable:
            NSLog(@"SKErrorStoreProductNotAvailable");
            detail = @"當前商品不可用";
            break;

        default:
            NSLog(@"No Match Found for error");
            detail = @"未知的錯誤";
            break;
    }

    [MBProgressHUD wyp_error:detail block:^{
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
        NSLog(@"http://----------------------------- 購買失敗 ------------------------------//");
        NSLog(@"http://----------------------------- 交易失敗訂單號開始 ------------------------------//");
        NSLog(@"transaction.payment.applicationUsername:%@",transaction.payment.applicationUsername);
        NSLog(@"http://----------------------------- 交易失敗訂單號結(jié)束 ------------------------------//");
    }];
}

// 付款成功后僚饭,請求服務(wù)端
- (void)purchasedTransaction:(SKPaymentTransaction *)transaction{
    NSLog(@"http://----------------------------- 交易成功訂單號開始 ------------------------------//");
    NSLog(@"transaction.payment.applicationUsername:%@",transaction.payment.applicationUsername);
    NSLog(@"http://----------------------------- 交易成功訂單號結(jié)束 ------------------------------//");

    NSLog(@"http://----------------------------- 購買成功驗證訂單 ------------------------------//");
    NSLog(@"http://----------------------------- 獲得憑證receipt ------------------------------//");

    // 獲取憑證
    NSData *data = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
    NSString *receipt = [data base64EncodedStringWithOptions:0];
    NSLog(@"http://----------------------------- receipt 開始 ------------------------------//");
    NSLog(@"receipt(base64String):%@",receipt);
    NSLog(@"http://----------------------------- receipt 結(jié)束 ------------------------------//");
    if (!receipt) {
        [MBProgressHUD wyp_error:@"獲取憑證失敗" block:^{
            return;
        }];
    }

    if (self.cash) {
        NSLog(@"http://----------------------------- 處理正常流程 ------------------------------//");
        NSMutableDictionary *requestContent = [NSMutableDictionary dictionary];
        [requestContent setValue:receipt forKey:@"receiptData"];
        [requestContent setValue:self.transactionID forKey:@"transactionId"];
        [requestContent setValue:self.paymentID forKey:@"paymentId"];

        //把這個信息存起到本地
        LxIapRequestContent *iapRequestContent = [LxIapRequestContent instanceWithDic:requestContent error:nil];
        [[LxArchiverManager shareManager] saveArchiverData:iapRequestContent andKey:transaction.transactionIdentifier];

        self.cash = NO;
        [self tellServerBuySuccessWithTradeInfo:requestContent transaction:transaction];
    }else{
        NSLog(@"http://----------------------------- 處理漏單流程 ------------------------------//");
        //從本地讀取交易信息
        LxIapRequestContent *iapRequestContent = [[LxArchiverManager shareManager] queryArchiverDataWithKey:transaction.transactionIdentifier];
        NSMutableDictionary *requestContent = [NSMutableDictionary dictionary];
        [requestContent setValue:receipt forKey:@"receiptData"];
        [requestContent setValue:iapRequestContent.transactionId forKey:@"transactionId"];
        [requestContent setValue:iapRequestContent.paymentId forKey:@"paymentId"];
        [self tellServerBuySuccessWithTradeInfo:requestContent transaction:transaction];
    }
}

#pragma mark - 請求服務(wù)端,記錄交易

// 驗證交易
- (void)tellServerBuySuccessWithTradeInfo:(NSMutableDictionary *)tradeInfo transaction:(SKPaymentTransaction *)transaction{
    [SignServiceVerifyReceiptTask startWithDic:tradeInfo success:^(NSDictionary *__data, id __task) {
        [[LxArchiverManager shareManager] clearArchiverDataWithKey:transaction.transactionIdentifier];
        BOOL result = [[__data objectForKey:@"payResult"] boolValue];
        NSString *msg = [__data objectForKey:@"message"];
        if(!result){
            [MBProgressHUD wyp_error:msg block:^{
                NSLog(@"http://----------------------------- 驗證失敗,購買結(jié)束 ------------------------------//");
                if (self.failureBlock) {
                    self.failureBlock(__data);
                }
                return ;
            }];
        }else{
            [MBProgressHUD wyp_success:msg block:^{
                NSLog(@"http://----------------------------- 驗證成功,購買結(jié)束 ------------------------------//");
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                if (self.successBlock) {
                    self.successBlock(__data);
                }
            }];
        }
    } failure:^(id __data, id __task) {
        [MBProgressHUD wyp_error:@"驗證失敗,購買結(jié)束" block:^{
            NSLog(@"http://----------------------------- 驗證失敗,訂單結(jié)束 ------------------------------//");
            if (self.failureBlock) {
                self.failureBlock(__data);
            }
        }];
    }];
}

@end

如有問題胧砰,歡迎留言討論

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鳍鸵,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子尉间,更是在濱河造成了極大的恐慌权纤,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乌妒,死亡現(xiàn)場離奇詭異,居然都是意外死亡外邓,警方通過查閱死者的電腦和手機撤蚊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來损话,“玉大人侦啸,你說我怎么就攤上這事∩デ梗” “怎么了光涂?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拧烦。 經(jīng)常有香客問我忘闻,道長,這世上最難降的妖魔是什么恋博? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任齐佳,我火速辦了婚禮私恬,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘炼吴。我一直安慰自己本鸣,他們只是感情好鸽粉,可當我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布桩皿。 她就那樣靜靜地躺著枪蘑,像睡著了一般怠堪。 火紅的嫁衣襯著肌膚如雪对供。 梳的紋絲不亂的頭發(fā)上吭从,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天土匀,我揣著相機與錄音周荐,去河邊找鬼辐脖。 笑死饲宛,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的嗜价。 我是一名探鬼主播艇抠,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼久锥!你這毒婦竟也來了家淤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤瑟由,失蹤者是張志新(化名)和其女友劉穎絮重,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體歹苦,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡青伤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了殴瘦。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狠角。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蚪腋,靈堂內(nèi)的尸體忽然破棺而出丰歌,到底是詐尸還是另有隱情,我是刑警寧澤屉凯,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布立帖,位于F島的核電站,受9級特大地震影響悠砚,放射性物質(zhì)發(fā)生泄漏晓勇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宵蕉。 院中可真熱鬧酝静,春花似錦、人聲如沸羡玛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽稼稿。三九已至薄榛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間让歼,已是汗流浹背敞恋。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留谋右,地道東北人硬猫。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像改执,于是被迫代替她去往敵國和親啸蜜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,573評論 2 359

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