iOS內(nèi)購(gòu)詳細(xì)教程&iOS內(nèi)購(gòu)坑

1榄檬、收款協(xié)議以及賬戶(hù)等的創(chuàng)建

內(nèi)購(gòu)收款協(xié)議等的創(chuàng)建卜范,這里一般由運(yùn)營(yíng)負(fù)責(zé),這里不做介紹鹿榜,但是如果想要了解先朦,請(qǐng)參考這位博主的文章,里面的圖文都解釋得很清楚犬缨。http://www.reibang.com/p/86ac7d3b593a

2、開(kāi)發(fā)者中心文件創(chuàng)建

要開(kāi)啟iOS內(nèi)購(gòu)功能棉浸,首先在apple develop 中心怀薛,先創(chuàng)建證書(shū)以及描述文件并包含內(nèi)購(gòu)功能。在項(xiàng)目中打開(kāi) In - App-purchase 功能即可繼續(xù)下面的代碼實(shí)現(xiàn)迷郑。

3枝恋、代碼實(shí)現(xiàn)

我這邊的代碼實(shí)現(xiàn)自己實(shí)現(xiàn)了一個(gè)工具類(lèi)创倔。然后內(nèi)購(gòu)的相關(guān)代碼以及邏輯在這個(gè)類(lèi)實(shí)現(xiàn),這樣做的好處是不需要在控制器中寫(xiě)過(guò)多的代碼焚碌,方便轉(zhuǎn)移使用畦攘,符合代碼高聚合性低耦合性的原則。

首先導(dǎo)入在項(xiàng)目的 Build Phases 下的Link Binary With libraires 中添加StoreKit.framework

在這個(gè)工具類(lèi)里面 十电,我寫(xiě)了一個(gè)單例方法,包括添加內(nèi)購(gòu)監(jiān)聽(tīng)知押,停止內(nèi)購(gòu)監(jiān)聽(tīng)以及發(fā)起內(nèi)購(gòu)購(gòu)買(mǎi)的方法,話(huà)不多說(shuō)直接上代碼 。

下面是IPAPurchase.h的代碼

#import <Foundation/Foundation.h>

/**
 block

 @param isSuccess 是否支付成功
 @param certificate 支付成功得到的憑證(用于在自己服務(wù)器驗(yàn)證)
 @param errorMsg 錯(cuò)誤信息
 */
typedef void(^PayResult)(BOOL isSuccess,NSString *certificate,NSString *errorMsg);

@interface IPAPurchase : NSObject
@property (nonatomic, copy)PayResult payResultBlock;

/**
單例方法
*/
+ (instancetype)manager;

/**
 開(kāi)啟內(nèi)購(gòu)監(jiān)聽(tīng) 在程序入口didFinishLaunchingWithOptions實(shí)現(xiàn)
 */
-(void)startManager;

/**
停止內(nèi)購(gòu)監(jiān)聽(tīng) 在AppDelegate.m中的applicationWillTerminate方法實(shí)現(xiàn)
*/
-(void)stopManager;

/**
 拉起內(nèi)購(gòu)支付
 @param productID 內(nèi)購(gòu)商品ID
 @param payResult 結(jié)果
 */

-(void)buyProductWithProductID:(NSString *)productID payResult:(PayResult)payResult;

以下是IPAPurchase.m文件的代碼

#import "IPAPurchase.h"
#import <StoreKit/StoreKit.h>

static NSString * const receiptKey = @"receipt_key";

dispatch_queue_t iap_queue() {
static dispatch_queue_t as_iap_queue;
static dispatch_once_t onceToken_iap_queue;
dispatch_once(&onceToken_iap_queue, ^{
    as_iap_queue = dispatch_queue_create("com.iap.queue", DISPATCH_QUEUE_CONCURRENT);
});
    return as_iap_queue;
 }

@interface IPAPurchase()<SKPaymentTransactionObserver,
                    SKProductsRequestDelegate>
{
      SKProductsRequest *request;
}

//購(gòu)買(mǎi)憑證
@property (nonatomic,copy)NSString *receipt;//存儲(chǔ)base64編碼的交易憑證

//產(chǎn)品ID
@property (nonnull,copy)NSString * profductId;

@end

static IPAPurchase * manager = nil;

@implementation IPAPurchase
#pragma mark -- 單例方法
+ (instancetype)manager{

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (!manager) {
        manager = [[IPAPurchase alloc] init];
    }
});   
     return manager;
}

#pragma mark -- 漏單處理
-(void)startManager{

dispatch_sync(iap_queue(), ^{

 [[SKPaymentQueue defaultQueue] addTransactionObserver:manager];

 });   
}

#pragma mark -- 移除交易事件

-(void)stopManager{

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];

});
}

#pragma mark -- 發(fā)起購(gòu)買(mǎi)的方法
-(void)buyProductWithProductID:(NSString *)productID  payResult:(PayResult)payResult{

self.payResultBlock = payResult;
//移除上次未完成的交易訂單
[self removeAllUncompleteTransactionBeforeStartNewTransaction];

[RRHUD showWithContainerView:RR_keyWindow status:NSLocalizedString(@"購(gòu)買(mǎi)中...", @"")];

self.profductId = productID;

if (!self.profductId.length) {

    UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"溫馨提示" message:@"沒(méi)有對(duì)應(yīng)的商品" delegate:nil cancelButtonTitle:@"確定" otherButtonTitles: nil];

    [alertView show];
}

if ([SKPaymentQueue canMakePayments]) {

    [self requestProductInfo:self.profductId];

}else{

    UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"溫馨提示" message:@"請(qǐng)先開(kāi)啟應(yīng)用內(nèi)付費(fèi)購(gòu)買(mǎi)功能鹃骂。" delegate:nil cancelButtonTitle:@"確定" otherButtonTitles: nil];

    [alertView show];

      }
}

#pragma mark -- 結(jié)束上次未完成的交易 防止串單
-(void)removeAllUncompleteTransactionBeforeStartNewTransaction{

NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
if (transactions.count > 0) {
    //檢測(cè)是否有未完成的交易
    SKPaymentTransaction* transaction = [transactions firstObject];
    if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
        return;
    }
}
}

#pragma mark -- 發(fā)起購(gòu)買(mǎi)請(qǐng)求
-(void)requestProductInfo:(NSString *)productID{

    NSArray * productArray = [[NSArray alloc]initWithObjects:productID,nil];

NSSet * IDSet = [NSSet setWithArray:productArray];

request = [[SKProductsRequest alloc]initWithProductIdentifiers:IDSet];

request.delegate = self;

[request start];

}

#pragma mark -- SKProductsRequestDelegate 查詢(xún)成功后的回調(diào)
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSArray *myProduct = response.products;

if (myProduct.count == 0) {

    [RRHUD hide];
    [RRHUD showErrorWithContainerView:UL_rootVC.view status:NSLocalizedString(@"沒(méi)有該商品信息", @"")];

    if (self.payResultBlock) {
        self.payResultBlock(NO, nil, @"無(wú)法獲取產(chǎn)品信息,購(gòu)買(mǎi)失敗");
    }

    return;
}

SKProduct * product = nil;
for(SKProduct * pro in myProduct){

    NSLog(@"SKProduct 描述信息%@", [pro description]);
    NSLog(@"產(chǎn)品標(biāo)題 %@" , pro.localizedTitle);
    NSLog(@"產(chǎn)品描述信息: %@" , pro.localizedDescription);
    NSLog(@"價(jià)格: %@" , pro.price);
    NSLog(@"Product id: %@" , pro.productIdentifier);

    if ([pro.productIdentifier isEqualToString:self.profductId]) {

        product = pro;
        break;

    }
}

if (product) {

SKMutablePayment * payment = [SKMutablePayment paymentWithProduct:product];
//內(nèi)購(gòu)?fù)競(jìng)鲄?shù)
payment.applicationUsername = self.order;

[[SKPaymentQueue defaultQueue] addPayment:payment];

}else{

    NSLog(@"沒(méi)有此商品");
   }
}

//查詢(xún)失敗后的回調(diào)
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {

if (self.payResultBlock) {
    self.payResultBlock(NO, nil, [error localizedDescription]);
      }
}

//如果沒(méi)有設(shè)置監(jiān)聽(tīng)購(gòu)買(mǎi)結(jié)果將直接跳至反饋結(jié)束台盯;
-(void)requestDidFinish:(SKRequest *)request{

}

#pragma mark -- 監(jiān)聽(tīng)結(jié)果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{

//當(dāng)用戶(hù)購(gòu)買(mǎi)的操作有結(jié)果時(shí),就會(huì)觸發(fā)下面的回調(diào)函數(shù)畏线,
for (SKPaymentTransaction * transaction in transactions) {

    switch (transaction.transactionState) {

        case SKPaymentTransactionStatePurchased:{

        [self completeTransaction:transaction];

        }break;

        case SKPaymentTransactionStateFailed:{

        [self failedTransaction:transaction];
        }break;

        case SKPaymentTransactionStateRestored:{//已經(jīng)購(gòu)買(mǎi)過(guò)該商品

        [RRHUD hide];
        [self restoreTransaction:transaction];

        }break;

        case SKPaymentTransactionStatePurchasing:{

            NSLog(@"正在購(gòu)買(mǎi)中...");

        }break;

        case SKPaymentTransactionStateDeferred:{

            NSLog(@"最終狀態(tài)未確定");

        }break;

        default:
            break;
      }
   }
}

//完成交易
#pragma mark -- 交易完成的回調(diào)
- (void)completeTransaction:(SKPaymentTransaction *)transaction{

NSLog(@"購(gòu)買(mǎi)成功,準(zhǔn)備驗(yàn)證發(fā)貨");
[self getReceipt]; //獲取交易成功后的購(gòu)買(mǎi)憑證
[self saveReceipt:transaction]; //存儲(chǔ)交易憑證
[self checkIAPFiles:transaction];
}

#pragma mark -- 處理交易失敗回調(diào)
- (void)failedTransaction:(SKPaymentTransaction *)transaction
{

[RRHUD hide];
NSString *error = nil;
if(transaction.error.code != SKErrorPaymentCancelled) {

    [RRHUD showInfoWithContainerView:UL_rootVC.view status:NSLocalizedString(@"購(gòu)買(mǎi)失敗", @"")];
} else {

    [RRHUD showInfoWithContainerView:UL_rootVC.view status:NSLocalizedString(@"取消購(gòu)買(mǎi)", @"")];
}

if (self.payResultBlock) {
    self.payResultBlock(NO, nil, error);
}

[[SKPaymentQueue defaultQueue]finishTransaction:transaction];

}

- (void)restoreTransaction:(SKPaymentTransaction *)transaction{

[[SKPaymentQueue defaultQueue] finishTransaction:transaction];

}

#pragma mark -- 獲取購(gòu)買(mǎi)憑證
-(void)getReceipt{

NSURL * receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
NSData * receiptData = [NSData dataWithContentsOfURL:receiptUrl];
NSString * base64String = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
 self.receipt = base64String;
}

#pragma mark -- 存儲(chǔ)購(gòu)買(mǎi)憑證
-(void)saveReceipt:(SKPaymentTransaction *)transaction{
NSString * userId;
NSString * order;

if (self.userid) {

    userId = self.userid;

    [[NSUserDefaults standardUserDefaults]setObject:userId forKey:@"unlock_iap_userId"];
}else{

    userId = [[NSUserDefaults standardUserDefaults]objectForKey:@"unlock_iap_userId"];
}

order = transaction.payment.applicationUsername;

NSString *fileName = [NSString UUID];
NSString *savedPath = [NSString stringWithFormat:@"%@/%@.plist", [SandBoxHelper iapReceiptPath], fileName];
NSMutableDictionary * dic = [[NSMutableDictionary alloc]init];
[dic setValue: self.receipt forKey:receiptKey];
[dic setValue: userId forKey:@"user_id"];
[dic setValue: order forKey:@"order"];
BOOL ifWriteSuccess  = [dic writeToFile:savedPath atomically:YES];

if (ifWriteSuccess) {

    NSLog(@"購(gòu)買(mǎi)憑據(jù)存儲(chǔ)成功!");
  }
}

#pragma mark -- 驗(yàn)證本地?cái)?shù)據(jù)
-(void)checkIAPFiles:(SKPaymentTransaction *)transaction{

NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
NSArray *cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:[SandBoxHelper iapReceiptPath] error:&error];

if (error == nil) {

    for (NSString *name in cacheFileNameArray) {

        if ([name hasSuffix:@".plist"]){ //如果有plist后綴的文件静盅,說(shuō)明就是存儲(chǔ)的購(gòu)買(mǎi)憑證

            NSString *filePath = [NSString stringWithFormat:@"%@/%@", [SandBoxHelper iapReceiptPath], name];

            [self sendAppStoreRequestBuyPlist:filePath trans:transaction];
        }
    }

} else {

  }
}

#pragma mark -- 存儲(chǔ)成功訂單
-(void)SaveIapSuccessReceiptDataWithReceipt:(NSString *)receipt Order:(NSString *)order UserId:(NSString *)userId{

NSMutableDictionary * mdic = [[NSMutableDictionary alloc]init];
[mdic setValue:[self getCurrentZoneTime] forKey:@"time"];
[mdic setValue: order forKey:@"order"];
[mdic setValue: userId forKey:@"userid"];
[mdic setValue: receipt forKey:receiptKey];
NSString *fileName = [NSString UUID];
NSString * successReceiptPath = [NSString stringWithFormat:@"%@/%@.plist", [SandBoxHelper SuccessIapPath], fileName];
//存儲(chǔ)購(gòu)買(mǎi)成功的憑證
[mdic writeToFile:successReceiptPath atomically:YES];

}

#pragma mark -- 獲取系統(tǒng)時(shí)間的方法
-(NSString *)getCurrentZoneTime{

NSDate * date = [NSDate date];
NSDateFormatter*formatter = [[NSDateFormatter alloc]init];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
NSString*dateTime = [formatter stringFromDate:date];
return dateTime;
}

#pragma mark -- 去服務(wù)器驗(yàn)證購(gòu)買(mǎi)
-(void)sendAppStoreRequestBuyPlist:(NSString *)plistPath trans:(SKPaymentTransaction *)transaction{

NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:plistPath];
NSString * receipt = [dic objectForKey:receiptKey];
NSString * order = [dic objectForKey:@"order"];
NSString * userId = [dic objectForKey:@"user_id"];

 #pragma mark -- 發(fā)送信息去驗(yàn)證是否成功
[[ULSDKAPI shareAPI] sendVertifyWithReceipt:receipt order:order success:^(ULSDKAPI *api, id responseObject) {

    if (RequestSuccess) {

        NSLog(@"服務(wù)器驗(yàn)證成功!");

        [[SKPaymentQueue defaultQueue]finishTransaction:transaction];

        [RRHUD hide];

        [RRHUD showSuccessWithContainerView:UL_rootVC.view status:NSLocalizedString(@"購(gòu)買(mǎi)成功", @"")];

        [[NSUserDefaults standardUserDefaults]removeObjectForKey:@"unlock_iap_userId"];
        NSData * data = [NSData dataWithContentsOfFile:[[[NSBundle mainBundle] appStoreReceiptURL] path]];

        NSString *result = [data base64EncodedStringWithOptions:0];

        if (self.payResultBlock) {
            self.payResultBlock(YES, result, nil);
        }
        //這里將成功但存儲(chǔ)起來(lái)
        [self SaveIapSuccessReceiptDataWithReceipt:receipt Order:order UserId:userId];

        [self successConsumptionOfGoodsWithOrder:order];

    }else{            
      //在這里向服務(wù)器發(fā)送驗(yàn)證失敗相關(guān)信息
} failure:^(ULSDKAPI *api, NSString *failure) {

}

#pragma mark -- 根據(jù)訂單號(hào)來(lái)移除本地憑證的方法
-(void)successConsumptionOfGoodsWithOrder:(NSString * )cpOrder{

NSFileManager *fileManager = [NSFileManager defaultManager];
NSError * error;
if ([fileManager fileExistsAtPath:[SandBoxHelper iapReceiptPath]]) {

    NSArray * cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:[SandBoxHelper iapReceiptPath] error:&error];

    if (error == nil) {

        for (NSString * name in cacheFileNameArray) {

            NSString * filePath = [NSString stringWithFormat:@"%@/%@", [SandBoxHelper iapReceiptPath], name];

            [self removeReceiptWithPlistPath:filePath ByCpOrder:cpOrder];

        }
    }
  }
}

#pragma mark -- 根據(jù)訂單號(hào)來(lái)刪除 存儲(chǔ)的憑證
-(void)removeReceiptWithPlistPath:(NSString *)plistPath   ByCpOrder:(NSString *)cpOrder{

NSFileManager *fileManager = [NSFileManager defaultManager];
NSError * error;
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:plistPath];
NSString * order = [dic objectForKey:@"order"];

if ([cpOrder isEqualToString:order]) {
    //移除與游戲cp訂單號(hào)一樣的plist 文件
    BOOL ifRemove =  [fileManager removeItemAtPath:plistPath error:&error];
    if (ifRemove) { 
        NSLog(@"成功訂單移除成功");
    }else{
        NSLog(@"成功訂單移除失敗");
    }
}else{
        NSLog(@"本地?zé)o與之匹配的訂單");
    }
}

接下來(lái)是遇到的坑與解決

坑 1

因?yàn)槲覀冏叩姆?wù)器驗(yàn)證發(fā)貨的流程,因此服務(wù)器驗(yàn)證這一步尤其重要寝殴。如果用戶(hù)付了款 蒿叠,但是沒(méi)有發(fā)貨的話(huà)那問(wèn)題就大了,客戶(hù)是無(wú)法忍受這種情況的(你丫的吞老子的錢(qián))蚣常。剛開(kāi)始的時(shí)候我是把以下結(jié)束交易的代碼 寫(xiě)到了購(gòu)買(mǎi)回調(diào)-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions 方法里市咽,導(dǎo)致一走到購(gòu)買(mǎi)回調(diào)里就告訴蘋(píng)果這個(gè)交易結(jié)束了。但是如果此時(shí)我們服務(wù)器沒(méi)收到購(gòu)買(mǎi)憑證或者中途出了問(wèn)題的話(huà)史隆,玩家是收不到購(gòu)買(mǎi)的東西的魂务。導(dǎo)致我們后臺(tái)沒(méi)有匹配的訂單號(hào),蘋(píng)果又沒(méi)有提供與我們平臺(tái)訂單號(hào)的匹配的參數(shù)泌射,導(dǎo)致無(wú)法確定是用戶(hù)充值了沒(méi)收到貨粘姜,還是用戶(hù)裝可憐來(lái)訛我們,這一度讓我們很痛苦熔酷,無(wú)奈只能告訴玩家去蘋(píng)果申請(qǐng)退款孤紧。經(jīng)過(guò)各種百度和研究實(shí)驗(yàn),所以在這里重點(diǎn)注意的是>苊亍:畔浴!躺酒!如果是后臺(tái)做驗(yàn)證的話(huà)請(qǐng)把以下代碼寫(xiě)到成功提交內(nèi)購(gòu)憑證到服務(wù)器后臺(tái)之后再結(jié)束這次交易押蚤。這樣確保后臺(tái)收到了憑證驗(yàn)證成功,因此每次用戶(hù)來(lái)問(wèn)我們?cè)趺礇](méi)收到貨或者什么的,我們都有據(jù)可循羹应。漏單率也大大降低揽碘。

[[SKPaymentQueue defaultQueue]finishTransaction:transaction];

坑2

因?yàn)槲沂窃谟螒蚬緦?xiě)SDK給研發(fā)用的,因此在給接口研發(fā)對(duì)接的時(shí)候就遇到一個(gè)問(wèn)題,就是研發(fā)接入的時(shí)候沒(méi)有實(shí)現(xiàn)內(nèi)購(gòu)監(jiān)聽(tīng)的代碼雳刺,也就是IPAPurchase.h 中的以下方法劫灶,我們看看這個(gè)方法是干什么的?蘋(píng)果的注釋是 Observers are not retained. The transactions array will only be synchronized with the server while the queue has observers. This may require that the user authenticate.假如我們沒(méi)寫(xiě)這個(gè)方法會(huì)怎樣掖桦?答案是:你購(gòu)買(mǎi)之后他壓根就沒(méi)走購(gòu)買(mǎi)的代理方法本昏,也就是說(shuō),他不會(huì)走-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions回調(diào)方法枪汪,就算你能獲取到商品又怎樣涌穆?反正你不添加監(jiān)聽(tīng),我就是不走料饥。這會(huì)導(dǎo)致什么問(wèn)題呢蒲犬?導(dǎo)致的問(wèn)題是:你購(gòu)買(mǎi)成功后的邏輯都不會(huì)走,你驗(yàn)證不了岸啡,你更發(fā)不了貨原叮。然而更加恐怖的是什么呢?假如不知道自己沒(méi)寫(xiě)巡蘸,不停地點(diǎn)擊購(gòu)買(mǎi)奋隶,買(mǎi)了100+次,這下你就攤上大事兒了悦荒。當(dāng)你知道自己沒(méi)寫(xiě)之后唯欣,將[[SKPaymentQueue defaultQueue] addTransactionObserver:self]代碼添加進(jìn)去之后 ,你會(huì)發(fā)現(xiàn)-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions這個(gè)方法被回調(diào)了100+次搬味。OMG!所以,記住了境氢,這個(gè)代碼最好在程序啟動(dòng)入口就實(shí)現(xiàn),這樣的話(huà)會(huì)在程序一進(jìn)來(lái)就去遍歷過(guò)往未完成的單碰纬。

-(void)startManager

坑3

我們假想一種情況萍聊,比如有兩個(gè)玩家A和B,首先A在應(yīng)用內(nèi)發(fā)起內(nèi)購(gòu)購(gòu)買(mǎi),成功了但是在去服務(wù)器驗(yàn)證的中間發(fā)生異常應(yīng)用退出了。也就是說(shuō)漏單了悦析,他買(mǎi)了東西沒(méi)有到賬寿桨。然后他找到B說(shuō),我曹强戴!我剛才買(mǎi)了東西亭螟,但是沒(méi)收到貨。我懷疑是不是我手機(jī)有問(wèn)題骑歹,我在你手機(jī)上登錄看看预烙,會(huì)不會(huì)到貨了。于是道媚,A在B的手機(jī)上登陸了自己的appid 并且進(jìn)入應(yīng)用內(nèi)發(fā)現(xiàn)們依然沒(méi)有到賬扁掸。于是他倆都把應(yīng)用刪除了欢嘿,然后從新下載安裝發(fā)現(xiàn),還是沒(méi)有到貨也糊。他們倆很氣,找到你們公司客服說(shuō):你丫的羡宙,怎么我買(mǎi)了東西都沒(méi)到賬狸剃。我都收到蘋(píng)果發(fā)送的憑據(jù)了。你們信不信我去工商局告你狗热?然后你們客服問(wèn)后臺(tái)說(shuō)钞馁,后臺(tái),他們說(shuō)他們已經(jīng)付款了匿刮,但是沒(méi)收到貨僧凰,你能查一下后臺(tái)有沒(méi)有對(duì)應(yīng)的訂單么?然后后臺(tái)趕緊去看一下熟丸,竟然沒(méi)有對(duì)應(yīng)的訂單训措。于是猜測(cè)說(shuō)他們是來(lái)訛我們的,不用管光羞。于是就這樣绩鸣,用戶(hù)付了錢(qián)沒(méi)收到東西,服務(wù)器端也找都不到對(duì)應(yīng)的訂單纱兑。大家相互猜疑和指責(zé)呀闻。問(wèn)題不了了之,對(duì)用戶(hù)而言潜慎,他們無(wú)辜的浪費(fèi)了金錢(qián)得不到東西捡多,體驗(yàn)很差。對(duì)公司而言铐炫,無(wú)法確認(rèn)問(wèn)題垒手,導(dǎo)致用戶(hù)的流失。這都是我們不想遇到的驳遵。那這個(gè)問(wèn)題的出現(xiàn)原因在哪里呢淫奔?首先,如果按照漏單流程來(lái)走的話(huà)堤结,獲取A用戶(hù)在下次進(jìn)應(yīng)用的時(shí)候就會(huì)收到東西唆迁,但是他沒(méi)有這樣而是在B的手機(jī)上登陸了APPid,也沒(méi)到賬的情況下,他們把應(yīng)用都刪除了竞穷。重點(diǎn)就是這一步唐责,刪除了。一般的補(bǔ)單流程是這樣的瘾带,如果沒(méi)告訴蘋(píng)果這筆訂單已經(jīng)完成鼠哥,那么下次進(jìn)來(lái)的時(shí)候,他會(huì)走一個(gè)補(bǔ)單的流程。也就是重新走 -(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions朴恳,成功后走服務(wù)器驗(yàn)證->驗(yàn)證成功之后發(fā)貨抄罕。但是如果刪除了應(yīng)用那問(wèn)題就不一樣了。假如于颖,你們后臺(tái)的驗(yàn)證流程是需要購(gòu)買(mǎi)憑證以及平臺(tái)訂單號(hào)的呆贿。那么如果刪除了應(yīng)用,那此時(shí)走補(bǔ)單的流程森渐,這個(gè)訂單號(hào)該怎么獲茸鋈搿?因?yàn)閯h除了應(yīng)用也就是說(shuō)同衣,之前存儲(chǔ)的訂單號(hào)都沒(méi)了竟块,那為什么B的手機(jī)也沒(méi)法補(bǔ)單成功呢?那是因?yàn)锽本來(lái)就沒(méi)存儲(chǔ)平臺(tái)訂單號(hào)耐齐。所以去服務(wù)器驗(yàn)證當(dāng)然也驗(yàn)證不過(guò)浪秘,因?yàn)槿鄙倨脚_(tái)訂單參數(shù),所以請(qǐng)求無(wú)法完成蚪缀。在iOS7 之前秫逝,針對(duì)這種情況,沒(méi)法解決這種情況询枚。但是在iOS7 之后违帆。蘋(píng)果新增了一個(gè)applicationUsername的屬性,那這個(gè)屬性是干嘛的金蜀?這個(gè)屬性是在創(chuàng)建內(nèi)購(gòu)支付的透?jìng)鲄?shù)刷后,在iOS 7 之后蘋(píng)果新增的。他的作用渊抄,在發(fā)起支付前把這個(gè)參數(shù)的值設(shè)置為平臺(tái)訂單號(hào)尝胆,是在購(gòu)買(mǎi)成功之后,這個(gè)參數(shù)原樣一并返回到回調(diào)方法的transcation 中护桦,通過(guò)transcation.payment.applicationUsername可以獲取到含衔,而且是每筆訂單一一對(duì)應(yīng)的。這樣我們?cè)趧?chuàng)建交易的時(shí)候加上這個(gè)參數(shù)二庵,這個(gè)參數(shù)的值為我們的平臺(tái)訂單號(hào)贪染,這樣我們的平臺(tái)訂單就能跟每筆內(nèi)購(gòu)交易對(duì)應(yīng)上了。

SKMutablePayment * payment = [SKMutablePayment paymentWithProduct:product];
//內(nèi)購(gòu)?fù)競(jìng)鲄?shù),與transaction一一對(duì)應(yīng)
 payment.applicationUsername = self.order;
[[SKPaymentQueue defaultQueue] addPayment:payment];

因此催享,無(wú)論我們?cè)谀桥_(tái)手機(jī)上登錄杭隙,都可以獲取到交易對(duì)應(yīng)的平臺(tái)訂單,也就可以向服務(wù)器驗(yàn)證成功了因妙。耶~~

坑4

當(dāng)你因?yàn)槟撤N原因購(gòu)買(mǎi)了東西痰憎,但是沒(méi)告訴蘋(píng)果這個(gè)交易已經(jīng)完成的時(shí)候再次發(fā)起購(gòu)買(mǎi)票髓,會(huì)發(fā)生什么事呢?你會(huì)發(fā)現(xiàn)出現(xiàn)一個(gè)提示“您已購(gòu)買(mǎi)此App內(nèi)購(gòu)買(mǎi)項(xiàng)目,此項(xiàng)目將會(huì)免費(fèi)恢復(fù)”铣耘。當(dāng)然出現(xiàn)這種的可能性不高洽沟,但是還是會(huì)有遇到。如果是消耗性的商品蜗细,如果不處理會(huì)導(dǎo)致這個(gè)內(nèi)購(gòu)項(xiàng)目一直無(wú)法購(gòu)買(mǎi)的問(wèn)題玲躯。那怎么處理呢?首先鳄乏,在以下方法中存儲(chǔ)著未完成的單,

NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;

在發(fā)起新的購(gòu)買(mǎi)之前棘利,我們先去檢查一下是否有已經(jīng)購(gòu)買(mǎi)成功但是未結(jié)束交易的單橱野,如果有的話(huà),實(shí)現(xiàn)以下代碼將未結(jié)束交易的單結(jié)束掉再發(fā)起新的購(gòu)買(mǎi)就OK 啦善玫。

  NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
if (transactions.count > 0) {
    //檢測(cè)是否有未完成的交易
    SKPaymentTransaction* transaction = [transactions firstObject];
    if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];            
        return;
    }
}

github 的直通鏈接 https://github.com/jiajiaailaras/ULIPAPurchase 覺(jué)得有幫助的水援。可以給個(gè)Star 支持一下茅郎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蜗元,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子系冗,更是在濱河造成了極大的恐慌奕扣,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掌敬,死亡現(xiàn)場(chǎng)離奇詭異惯豆,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)奔害,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)楷兽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人华临,你說(shuō)我怎么就攤上這事芯杀。” “怎么了雅潭?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵揭厚,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我寻馏,道長(zhǎng)棋弥,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任诚欠,我火速辦了婚禮顽染,結(jié)果婚禮上漾岳,老公的妹妹穿的比我還像新娘。我一直安慰自己粉寞,他們只是感情好尼荆,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著唧垦,像睡著了一般捅儒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上振亮,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天巧还,我揣著相機(jī)與錄音,去河邊找鬼坊秸。 笑死麸祷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的褒搔。 我是一名探鬼主播阶牍,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼星瘾!你這毒婦竟也來(lái)了走孽?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤琳状,失蹤者是張志新(化名)和其女友劉穎磕瓷,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體念逞,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡生宛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肮柜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陷舅。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖审洞,靈堂內(nèi)的尸體忽然破棺而出莱睁,到底是詐尸還是另有隱情,我是刑警寧澤芒澜,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布仰剿,位于F島的核電站,受9級(jí)特大地震影響痴晦,放射性物質(zhì)發(fā)生泄漏南吮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一誊酌、第九天 我趴在偏房一處隱蔽的房頂上張望部凑。 院中可真熱鬧露乏,春花似錦、人聲如沸涂邀。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)比勉。三九已至劳较,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浩聋,已是汗流浹背观蜗。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留衣洁,地道東北人嫂便。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像闸与,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子岸售,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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