蘋(píng)果內(nèi)購(gòu)(IAP)及掉單處理

官方文檔In-App Purchase

內(nèi)購(gòu)的前期準(zhǔn)備等工作本文不講述,有需要的可以查看網(wǎng)上其他文章,不少講的挺詳細(xì)的。
首先Xcode里的Capabilities中的In-App Purchase的能力打開(kāi), 如下圖
image.png

關(guān)于內(nèi)支付的文章網(wǎng)上很多,解決掉單問(wèn)題的文章及方案也是一搜一大堆,但是文章中講的掉單解決方案的實(shí)施難易程度以及可行性或者說(shuō)是否適合你的產(chǎn)品就需要你自己做好判斷了。我寫(xiě)這篇文章一方面總結(jié)下之前自身解決內(nèi)購(gòu)掉單的經(jīng)驗(yàn)另外希望能幫得到需要的人就更好了。

上代碼前先講一下我們產(chǎn)品的充值流程:
app端根據(jù)用戶選擇的商品ID發(fā)起請(qǐng)求(請(qǐng)求蘋(píng)果后臺(tái)商品) -> 請(qǐng)求回調(diào)中找到與剛商品的ID一致的產(chǎn)品然后發(fā)送購(gòu)買(mǎi)請(qǐng)求 -> 監(jiān)聽(tīng)購(gòu)買(mǎi)結(jié)果回調(diào)中狀態(tài)為SKPaymentTransactionStatePurchased(即交易完成)時(shí),調(diào)用自己服務(wù)接口將蘋(píng)果回調(diào)給的憑據(jù)傳給服務(wù)端 -> 服務(wù)端驗(yàn)證憑據(jù)成功后將用戶充值的商品分發(fā)給該賬戶下


對(duì)于內(nèi)購(gòu),我寫(xiě)了一個(gè)單例(建議單例,保證全局只有一個(gè)內(nèi)購(gòu)監(jiān)聽(tīng))

單例.h文件:
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface IWRechargeTool : NSObject

+ (instancetype)sharedInstance;

/**
 掉單處理
 */
- (void)checkIAPHandle;

/**
 內(nèi)購(gòu)

 @param goodsId 商品ID
 */
- (void)iapHandleWithGoodsId:(NSString *)goodsId;

@end

NS_ASSUME_NONNULL_END
.m文件

內(nèi)購(gòu)StoreKit肯定是要添加的

#import <StoreKit/StoreKit.h>

添加內(nèi)購(gòu)監(jiān)聽(tīng)與代理:

SKPaymentTransactionObserver, SKProductsRequestDelegate
  • 這里添加observerExist只是為了確保監(jiān)聽(tīng)始終只有一個(gè),loading為了確保loading的顯示
static IWRechargeTool *tool = nil;

@interface IWRechargeTool ()<SKPaymentTransactionObserver, SKProductsRequestDelegate>

@property (nonatomic, copy) NSString *goodsId;
@property (nonatomic, assign) BOOL observerExist;//觀察是否存在 YES(存在) NO(不存在 默認(rèn))
@property (nonatomic, assign) BOOL loading;//loading是否存在 YES(存在) NO(不存在 默認(rèn))

@end
+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        tool = [[IWRechargeTool alloc] init];
    });
    
    return tool;
}

- (instancetype)init
{
    if (self = [super init]) {
        // 3.設(shè)置支付服務(wù) 監(jiān)聽(tīng)
        NSLog(@"==tool init");
        self.observerExist = NO;
        self.loading = NO;
        [self addIAPObserverHandle];
    }
    return self;
}

請(qǐng)不要吐槽單例寫(xiě)法_

這個(gè)方法可以理解為用戶選擇了某一商品并且點(diǎn)擊了購(gòu)買(mǎi)按鈕:

- (void)iapHandleWithGoodsId:(NSString *)goodsId
{
    NSLog(@"內(nèi)支付開(kāi)始: goodsId: %@", goodsId);
    if (StrEmpty(goodsId)) {
        return;
    }
    self.goodsId = goodsId;
    
    [self addLoadingHandle];
    [self addIAPObserverHandle];
    
    // 5.點(diǎn)擊按鈕的時(shí)候判斷app是否允許apple支付
    //如果app允許applepay
    if ([SKPaymentQueue canMakePayments]) {
        NSLog(@"==canMakePayments");
        NSLog(@"==goodsId: %@", self.goodsId);
        // 6.請(qǐng)求蘋(píng)果后臺(tái)商品
        [self getRequestAppleProduct];
    } else {
        NSLog(@"not canMakePayments");
        [self removeLoadingHandle];
        [self removeIAPObserverHandle];
        [MBProgressHUD showToast:@"請(qǐng)打開(kāi)Apple支付"];
    }
}
//請(qǐng)求蘋(píng)果商品
- (void)getRequestAppleProduct
{
    NSLog(@"====請(qǐng)求蘋(píng)果商品");
    // 7.這里的goodId就對(duì)應(yīng)著蘋(píng)果后臺(tái)的商品ID,他們是通過(guò)這個(gè)ID進(jìn)行聯(lián)系的。
    NSArray *product = [[NSArray alloc] initWithObjects:self.goodsId, nil];
    NSSet *nsset = [NSSet setWithArray:product];
    
    // 8.初始化請(qǐng)求
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
    request.delegate = self;
    
    // 9.開(kāi)始請(qǐng)求
    [request start];
}
// 10.接收到產(chǎn)品的返回信息,然后用返回的商品信息進(jìn)行發(fā)起購(gòu)買(mǎi)請(qǐng)求
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    NSLog(@"無(wú)效商品列表: %@", response.invalidProductIdentifiers);
    NSArray *product = response.products;
    //如果服務(wù)器沒(méi)有產(chǎn)品
    if([product count] == 0){
        NSLog(@"nothing");
        [self removeLoadingHandle];
        [self removeIAPObserverHandle];
        [MBProgressHUD showToast:@"沒(méi)有有效商品"];
        return;
    }
    
    NSLog(@"====product count: %lu", (unsigned long)product.count);
    
    SKProduct *requestProduct = nil;
    for (SKProduct *pro in product) {
        
        NSLog(@"%@", [pro description]);
        NSLog(@"%@", [pro localizedTitle]);
        NSLog(@"%@", [pro localizedDescription]);
        NSLog(@"%@", [pro price]);
        NSLog(@"%@", [pro productIdentifier]);
        
        // 11.如果后臺(tái)消費(fèi)條目的ID與我這里需要請(qǐng)求的一樣(用于確保訂單的正確性)
        if([pro.productIdentifier isEqualToString:self.goodsId]){
            
            [self addLoadingHandle];
            [self addIAPObserverHandle];
            
            requestProduct = pro;
            // 12.發(fā)送購(gòu)買(mǎi)請(qǐng)求
            SKPayment *payment = [SKPayment paymentWithProduct:requestProduct];
            [[SKPaymentQueue defaultQueue] addPayment:payment];
            NSLog(@"goodsId: %@", self.goodsId);
            NSLog(@"======addPayment");
            break;
        }
    }
}
// 13.監(jiān)聽(tīng)購(gòu)買(mǎi)結(jié)果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
    
    NSLog(@"==監(jiān)聽(tīng)購(gòu)買(mǎi)結(jié)果==");
    [self addLoadingHandle];
    [self addIAPObserverHandle];
    
    for(SKPaymentTransaction *tran in transactions){
        NSLog(@"==%@", tran.payment.productIdentifier);
        switch (tran.transactionState) {
            case SKPaymentTransactionStatePurchased:
                NSLog(@"交易完成");
                [self completeTransaction:tran];
                break;
            case SKPaymentTransactionStatePurchasing:
                NSLog(@"商品添加進(jìn)列表=正在購(gòu)買(mǎi)");
                [MBProgressHUD showToast:@"正在購(gòu)買(mǎi)"];
                break;
            case SKPaymentTransactionStateRestored:
                NSLog(@"已經(jīng)購(gòu)買(mǎi)過(guò)商品");
                [self removeLoadingHandle];
                [self removeIAPObserverHandle];
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                break;
            case SKPaymentTransactionStateFailed:
                NSLog(@"交易失敗");
                [self removeLoadingHandle];
                [self removeIAPObserverHandle];
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                [MBProgressHUD showToast:@"交易失敗"];
                break;
            case SKPaymentTransactionStateDeferred:
                NSLog(@"==還在隊(duì)列里 狀態(tài)還未決定");
                [MBProgressHUD showToast:@"正在購(gòu)買(mǎi)..."];
                break;
            default:
                NSLog(@"==updatedTransactions default");
                break;
        }
    }
}
  • 無(wú)論前端是否驗(yàn)證返回的憑據(jù),后臺(tái)均需要驗(yàn)證,因此前端我選擇不驗(yàn)證憑據(jù)
// 14.交易結(jié)束,當(dāng)交易結(jié)束后還要去appstore上驗(yàn)證支付信息是否都正確,只有所有都正確后,我們就可以給用戶方法我們的虛擬物品了。
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
    // 驗(yàn)證憑據(jù),獲取到蘋(píng)果返回的交易憑據(jù)
    // appStoreReceiptURL iOS7.0增加的,購(gòu)買(mǎi)交易完成后,會(huì)將憑據(jù)存放在該地址
    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    // 從沙盒中獲取到購(gòu)買(mǎi)憑據(jù)
    NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
    /**
     20      BASE64 常用的編碼方案,通常用于數(shù)據(jù)傳輸,以及加密算法的基礎(chǔ)算法,傳輸過(guò)程中能夠保證數(shù)據(jù)傳輸?shù)姆€(wěn)定性
     21      BASE64是可以編碼和解碼的
     22      */
    NSString *encodeStr = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    [self getApplePayDataToServerRequsetWith:transaction sendString:encodeStr];
}
注意如下的[[SKPaymentQueue defaultQueue] finishTransaction:transaction] 與發(fā)送購(gòu)買(mǎi)請(qǐng)求的[[SKPaymentQueue defaultQueue] addPayment:payment]必須確保成對(duì)出現(xiàn),若有未結(jié)束掉的,每次添加內(nèi)購(gòu)監(jiān)聽(tīng),回調(diào)結(jié)果中均會(huì)將未結(jié)束掉的事務(wù)回調(diào)到app端;
解決掉單也是依據(jù)該機(jī)制來(lái)處理的
- (void)getApplePayDataToServerRequsetWith:(SKPaymentTransaction *)transaction sendString:(NSString *)sendString{
    
    NSLog(@"==========getApplePayDataToServerRequsetWith=========");
    NSLog(@"==憑據(jù): %@", sendString);
    NSMutableDictionary *parms = [NSMutableDictionary dictionary];
    /*
     receipt    String    是    蘋(píng)果支付后返回的簽名字符串    -
     */
    parms[@"receipt"] = sendString;
    
    WEAKSELF
    [IWNetworkManager postDataWithUrl:kPayApplePay_update parameters:parms type:IWLoadingTypeAll activityInView:nil alertMessage:nil success:^(id response, NSInteger resultCode, NSString *resultMessage) {
        if (resultCode == k200) {
            
            [[NSNotificationCenter defaultCenter] postNotificationName:kUpdateUserInfoNotification object:nil];
            [[NSNotificationCenter defaultCenter] postNotificationName:kMissingOrderHandleNotification object:nil];
            [weakSelf removeLoadingHandle];
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            [weakSelf removeIAPObserverHandle];
            [MBProgressHUD showToast:@"購(gòu)買(mǎi)成功"];
        } else {
            [weakSelf removeLoadingHandle];
            [MBProgressHUD showToast:@"購(gòu)買(mǎi)失敗"];
        }
    } failure:^(NSError *error) {
        [weakSelf removeLoadingHandle];
        [MBProgressHUD showToast:@"購(gòu)買(mǎi)失敗, 請(qǐng)重啟App等待數(shù)秒或聯(lián)系客服"];
    }];
}
//請(qǐng)求失敗
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
    [self removeLoadingHandle];
    [self removeIAPObserverHandle];
    NSLog(@"error: %@", error);
    [MBProgressHUD showToast:@"支付請(qǐng)求失敗"];
}

//反饋請(qǐng)求的產(chǎn)品信息結(jié)束后
- (void)requestDidFinish:(SKRequest *)request{
    NSLog(@"信息反饋結(jié)束");
}
- (void)removeIAPObserverHandle
{
    NSLog(@"removeIAPObserverHandle");
    if (self.observerExist) {
        NSLog(@"==removeIAPObserverHandle");
        self.observerExist = NO;
        [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
    }
}

- (void)addIAPObserverHandle
{
    NSLog(@"addIAPObserverHandle");
    if (!self.observerExist) {
        NSLog(@"==addIAPObserverHandle");
        self.observerExist = YES;
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
}

- (void)removeLoadingHandle
{
    NSLog(@"removeLoadingHandle");
    if (self.loading) {
        NSLog(@"==removeLoadingHandle");
        self.loading = NO;
        [MBProgressHUD hideHUDForView:[UIApplication sharedApplication].keyWindow animated:YES];
    }
}

- (void)addLoadingHandle
{
    NSLog(@"addLoadingHandle");
    if (!self.loading) {
        NSLog(@"==addLoadingHandle");
        self.loading = YES;
        [MBProgressHUD showHUDWithMessage:nil];
    }
}

掉單處理
其實(shí)只是添加了內(nèi)購(gòu)監(jiān)聽(tīng),若有未結(jié)束掉的事務(wù),則添加了內(nèi)購(gòu)監(jiān)聽(tīng)后,蘋(píng)果會(huì)將未結(jié)束掉的事務(wù)通過(guò)- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions回調(diào)到app,再進(jìn)行相應(yīng)處理
/**
 掉單處理
 */
- (void)checkIAPHandle
{
    [self addIAPObserverHandle];
}

在你認(rèn)為合適的地方添加內(nèi)購(gòu)監(jiān)聽(tīng),比如App啟動(dòng)時(shí),或者某些頁(yè)面,又或者你可以自行添加個(gè)定時(shí)器等進(jìn)行相應(yīng)的修改:

///掉單處理
[[IWRechargeTool sharedInstance] checkIAPHandle];

PS: 如果你們的內(nèi)購(gòu)流程需要app端將蘋(píng)果返回的購(gòu)買(mǎi)憑據(jù)和與其對(duì)應(yīng)的一個(gè)唯一值(比如orderId,接下來(lái)我暫且稱為orderId)傳輸給后臺(tái),建議和后臺(tái)重新設(shè)計(jì)一下內(nèi)購(gòu)流程。
因?yàn)槿绻切枰銓rderId和與其對(duì)應(yīng)的transaction傳給后臺(tái)的話,因?yàn)榭赡艽嬖诙鄠€(gè)掉單情況,那么你可能需要將所有你沒(méi)成功的orderId與其對(duì)應(yīng)的transaction都保存下來(lái),在某些時(shí)機(jī)嘗試將其再發(fā)送給后臺(tái);但這樣做 不僅前端的工作很累,并且不能夠處理所有的掉單,比如你FaceID/TouchID通過(guò)后,在監(jiān)聽(tīng)支付回調(diào)還未回調(diào)到app端時(shí),殺掉了app,此時(shí)雖然app進(jìn)程結(jié)束了,但用戶付款申請(qǐng)已經(jīng)發(fā)出,有可能用戶被扣款,但app端并未將該orderId對(duì)應(yīng)的transaction保存下來(lái),也未將此次購(gòu)買(mǎi)行為告訴自己后臺(tái),用戶購(gòu)買(mǎi)的商品也就不會(huì)到賬,因?yàn)槲幢4嬖搕ransaction你也無(wú)法對(duì)其發(fā)起重試。

IAP機(jī)制,只要你加了監(jiān)聽(tīng),檢測(cè)到有未結(jié)束的事務(wù),在- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions方法中就會(huì)將未結(jié)束掉的再次返回給你

設(shè)計(jì)的內(nèi)購(gòu)流程,應(yīng)該做到你只需要將憑據(jù)傳輸給自己服務(wù),后臺(tái)就可以通過(guò)驗(yàn)證得知該憑據(jù)是否有效以及用戶購(gòu)買(mǎi)的哪個(gè)商品(用戶信息是請(qǐng)求頭里通過(guò)token獲取的)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容