iOS開發(fā)內(nèi)購(In-App Purchase)總結(jié)

流程梳理

內(nèi)購思維導(dǎo)圖

一寨典、內(nèi)購類型介紹

iap.png

這四種內(nèi)購氛雪,使用過消耗型商品以及自動(dòng)續(xù)期訂閱類型,接入方式以及驗(yàn)證方式基本一致耸成,自動(dòng)續(xù)訂要關(guān)注的點(diǎn)有點(diǎn)多报亩,其中免費(fèi)試用期,用戶取消訂閱墓猎,以及恢復(fù)訂閱(自動(dòng)續(xù)訂蘋果強(qiáng)制要求有恢復(fù)按鈕)捆昏。

二、內(nèi)購流程

1.創(chuàng)建商品
內(nèi)購商品信息.png
內(nèi)購設(shè)置信息.png
內(nèi)購審核樣式.png
2.App Store Connect????用戶和訪問????沙盒右邊添加沙盒測(cè)試人員
沙盒測(cè)試賬號(hào).png
3.協(xié)議毙沾、稅務(wù)和銀行業(yè)務(wù)(需要在測(cè)試之前填寫骗卜,不然喚起支付的回調(diào)里拿不到內(nèi)購商品信息
協(xié)議、稅務(wù)和銀行業(yè)務(wù).png

協(xié)議狀態(tài).png

銀行信息.png

稅務(wù).png
4.代碼調(diào)試

步驟如下:

  • 獲取商品列表,從app內(nèi)讀取或者從自己服務(wù)器讀瓤懿帧举户;
  • 用戶選擇了商品,喚起支付請(qǐng)求遍烦,收到購買完成的回調(diào)俭嘁;
  • 購買流程結(jié)束后,向服務(wù)器發(fā)起驗(yàn)證憑證服猪,以及發(fā)放虛擬商品(驗(yàn)證時(shí)開發(fā)階段需要用沙盒環(huán)境供填,蘋果推薦使用App Store環(huán)境,當(dāng)返回21007時(shí)再進(jìn)行沙盒環(huán)境驗(yàn)證)罢猪;
  • 服務(wù)端認(rèn)證分為4步
    • 接受iOS端發(fā)來的購買憑證近她;
    • 判斷憑證是否已存在或驗(yàn)證過,然后存儲(chǔ)該憑證膳帕;
    • 將該憑證發(fā)送都蘋果的服務(wù)器驗(yàn)證粘捎,并將驗(yàn)證結(jié)果返回給客戶端;(考慮到網(wǎng)絡(luò)異常情況危彩,服務(wù)器的驗(yàn)證應(yīng)該是一個(gè)可恢復(fù)的隊(duì)列攒磨,如果網(wǎng)絡(luò)失敗了,應(yīng)該進(jìn)行重試汤徽。 簡(jiǎn)單來說就是將該購買憑證用Base64編碼娩缰,然后POST給蘋果的驗(yàn)證服務(wù)器,蘋果將驗(yàn)證結(jié)果以JSON形式返回
#import "OpenMemberManager.h"
#import <StoreKit/StoreKit.h>

#define kMoonGuardianMemberOfMonth @"com.plw.moonGuardian_vip_month"
#define kMoonGuardianMemberOfQuarter @"com.plw.moonGuardian_vip_quarter"
#define kMoonGuardianMemberOfYear @"com.plw.moonGuardian_vip_year"
#define IAPshareSecret @"946af2dda1874a8fbd1dc5beec78fe48"
#define SandboxVerifyReceipt @"https://sandbox.itunes.apple.com/verifyReceipt"
#define AppstoreVerifyReceipt @"https://buy.itunes.apple.com/verifyReceipt"

@interface OpenMemberManager ()<SKProductsRequestDelegate, SKPaymentTransactionObserver>

@property (nonatomic, strong) SKProductsRequest *request;
@property (nonatomic, copy) NSString * productID;       // 訂閱商品ID
@property (nonatomic, strong) MBProgressHUD *hudProgress;

@property (nonatomic, assign) IAPType iapType;          // 當(dāng)前購買類型
@property (nonatomic, copy) void(^Subscribe)(void);     // 內(nèi)購回調(diào)
@property (nonatomic, assign) BOOL verifyVip;           // 是否開始驗(yàn)證
@property (nonatomic, assign) BOOL isSubscribing;       // 正在進(jìn)行訂閱操作
@property (nonatomic, assign) BOOL isRestoring;         // 正在進(jìn)行還原操作

@end

@implementation OpenMemberManager

+ (instancetype)sharedInstance {
    static OpenMemberManager * manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[OpenMemberManager alloc] init];
        [manager startPaymentConfig];
    });
    return manager;
}

- (void)startPaymentConfig {
    self.verifyVip = NO;
    self.isSubscribing = NO;
    self.isRestoring = NO;
    //一定要 開啟內(nèi)購檢測(cè)
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    
    [[[UIApplication sharedApplication] keyWindow] addSubview:self.hudProgress];
}

// 恢復(fù)方法
- (void)restoreAction {
    //調(diào)起蘋果內(nèi)購恢復(fù)接口
    [self.hudProgress showAnimated:YES];
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
    self.isRestoring = YES;
    self.isSubscribing = NO;
}

// 訂閱方法
- (void)subscribeAction:(IAPType)iapType handle:(void(^)(void))result {
    self.iapType = iapType;
    self.Subscribe = result;
    self.verifyVip = NO;
    self.isSubscribing = YES;
    self.isRestoring = NO;

    NSString *proID;
    switch (iapType) {
        case IAPTypeOfMonth:
            proID = kMoonGuardianMemberOfMonth;
            break;
        case IAPTypeOfQuarter:
            proID = kMoonGuardianMemberOfQuarter;
            break;
        case IAPTypeOfYear:
            proID = kMoonGuardianMemberOfYear;
            break;
        default:
            break;
    }
    if ([SKPaymentQueue canMakePayments]) {
        self.productID = proID;
        [self requestProductData:proID];
        
    }else{
        dispatch_async(dispatch_get_main_queue(), ^{
            [PLWToast showCenterWithText:@"不允許程序內(nèi)付費(fèi)"];
        });
    }
}

// 收到請(qǐng)求信息
- (void)requestProductData:(NSString *)productID{
    NSLog(@"-------------請(qǐng)求對(duì)應(yīng)的產(chǎn)品信息----------------");
    [self.hudProgress showAnimated:YES];
    self.hudProgress.label.text = @"加載內(nèi)購信息...";
    
    NSArray *product = [[NSArray alloc] initWithObjects:productID,nil];
    
    NSSet *nsset = [NSSet setWithArray:product];
    _request = [[SKProductsRequest alloc]initWithProductIdentifiers:nsset];
    _request.delegate = self;
    [_request start];
}

// 收到返回信息
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    NSArray *product = response.products;
    if (product.count == 0) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.hudProgress hideAnimated:YES];
            [PLWToast showCenterWithText:@"購買失敗"];
        });
        return;
    }
    
    SKProduct *prod = nil;
    for (SKProduct *pro in product) {
        if ([pro.productIdentifier isEqualToString:self.productID]) {
            prod = pro;
        }
    }
    
    // 發(fā)送購買請(qǐng)求
    if (prod != nil) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.hudProgress.label.text = @"喚起支付...";
        });
        SKPayment *payment = [SKPayment paymentWithProduct:prod];
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
}

// 失敗回調(diào)
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.hudProgress hideAnimated:YES];
        [PLWToast showCenterWithText:@"購買失敗"];
    });
}
// 支付后的反饋信息
- (void)requestDidFinish:(SKRequest *)request{
    dispatch_async(dispatch_get_main_queue(), ^{
        self.hudProgress.label.text = @"";
    });
}

// 監(jiān)聽購買結(jié)果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
    for (SKPaymentTransaction *tran in transactions) {
        switch (tran.transactionState) {
            case SKPaymentTransactionStatePurchased:    // 交易完成
                if (tran.originalTransaction) { // 如果是自動(dòng)續(xù)費(fèi)的訂單originalTransaction會(huì)有內(nèi)容
                    NSLog(@"自動(dòng)續(xù)費(fèi)的訂單originalTransaction會(huì)有內(nèi)容");
                } else {  // 普通購買泻骤,以及 第一次購買 自動(dòng)訂閱
                    NSLog(@"普通購買漆羔,以及 第一次購買 自動(dòng)訂閱");
                }
                [self verifyPurchaseWithPaymentTransactionWith:tran];
                
                break;
            case SKPaymentTransactionStatePurchasing: {  //
                dispatch_async(dispatch_get_main_queue(), ^{
                    self.hudProgress.label.text = @"加載中...";
                });
                NSLog(@"商品已經(jīng)添加進(jìn)列表");
            }
                break;
                
            case SKPaymentTransactionStateRestored: {
                NSLog(@"已經(jīng)購買過商品");
                if (!self.verifyVip) {  // 驗(yàn)證一次,已購商品會(huì)有多個(gè)狱掂,避免重復(fù)驗(yàn)證
                    self.verifyVip = YES;
                    [self verifyPurchaseWithPaymentTransactionWith:tran];
                } else {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [self.hudProgress hideAnimated:YES];
                    });
                    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                }
                
                break;
            }
            case SKPaymentTransactionStateFailed:{
                NSLog(@"購買失敗");
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self.hudProgress hideAnimated:YES];
                    [PLWToast showCenterWithText:@"購買失敗"];
                });
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];

                break;
            }
            default: {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self.hudProgress hideAnimated:YES];
                });
                break;
            }
        }
    }
}

- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error API_AVAILABLE(ios(3.0), macos(10.7), watchos(6.2)) {
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.hudProgress hideAnimated:YES];
        [PLWToast showCenterWithText:@"恢復(fù)失敗"];
    });
}

// Sent when all transactions from the user's purchase history have successfully been added back to the queue.
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue API_AVAILABLE(ios(3.0), macos(10.7), watchos(6.2)) {
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.hudProgress hideAnimated:YES];
    });
}


/**
 *  驗(yàn)證購買演痒,避免越獄軟件模擬蘋果請(qǐng)求達(dá)到非法購買問題
 *
 */
-(void)verifyPurchaseWithPaymentTransactionWith:(SKPaymentTransaction *)tran{
    WeakSelf
    dispatch_async(dispatch_get_main_queue(), ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            self.hudProgress.label.text = @"驗(yàn)證內(nèi)購信息";
        });
    });
    
    NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];
    NSString *receiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];

    if (!receiptString) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [weakSelf.hudProgress hideAnimated:YES];
        });
        return;
    }
    [LoginManager iapVerifyRecipt:receiptString productId:self.productID result:^(NSDictionary * _Nullable resultDic) {
        [[SKPaymentQueue defaultQueue] finishTransaction:tran];
        dispatch_async(dispatch_get_main_queue(), ^{
            [weakSelf.hudProgress hideAnimated:YES];
        });
        if ([resultDic getInteger:@"code"] == 200) {   // 訂閱成功
            if (weakSelf.Subscribe) {
                weakSelf.Subscribe();
            }
            [DeviceControlManager flurryLogEvent:@"App_open_vip_success" withParameters:@{}];
        } else {
            [DeviceControlManager flurryLogEvent:@"App_open_vip_failure" withParameters:@{@"msg" : [resultDic getString:@"msg"]}];
        }
    } error:^(NSError * _Nonnull error) {
        [[SKPaymentQueue defaultQueue] finishTransaction:tran];
    }];
}

- (void)analysisInAppPurchasedData:(NSDictionary *)iapDic type:(IAPType)iapType {
    
}


#pragma mark - Getter
- (MBProgressHUD *)hudProgress {
    if (!_hudProgress) {
        _hudProgress = [[MBProgressHUD alloc] init];
    }
    return _hudProgress;
}

@end
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市趋惨,隨后出現(xiàn)的幾起案子鸟顺,更是在濱河造成了極大的恐慌,老刑警劉巖器虾,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讯嫂,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡兆沙,警方通過查閱死者的電腦和手機(jī)欧芽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來葛圃,“玉大人千扔,你說我怎么就攤上這事憎妙。” “怎么了曲楚?”我有些...
    開封第一講書人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵厘唾,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我龙誊,道長(zhǎng)抚垃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任趟大,我火速辦了婚禮鹤树,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘逊朽。我一直安慰自己魂迄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開白布惋耙。 她就那樣靜靜地躺著,像睡著了一般熊昌。 火紅的嫁衣襯著肌膚如雪绽榛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,785評(píng)論 1 314
  • 那天婿屹,我揣著相機(jī)與錄音灭美,去河邊找鬼。 笑死昂利,一個(gè)胖子當(dāng)著我的面吹牛届腐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蜂奸,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼犁苏,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了扩所?” 一聲冷哼從身側(cè)響起围详,我...
    開封第一講書人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎祖屏,沒想到半個(gè)月后助赞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡袁勺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年雹食,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片期丰。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡群叶,死狀恐怖吃挑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情盖呼,我是刑警寧澤儒鹿,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站几晤,受9級(jí)特大地震影響约炎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蟹瘾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一圾浅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧憾朴,春花似錦狸捕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至砾省,卻和暖如春鸡岗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背编兄。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來泰國打工轩性, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狠鸳。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓揣苏,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親件舵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子卸察,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

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