iOS開發內購(In-App Purchase)總結

流程梳理

內購思維導圖

一、內購類型介紹

iap.png

這四種內購,使用過消耗型商品以及自動續期訂閱類型,接入方式以及驗證方式基本一致,自動續訂要關注的點有點多,其中免費試用期,用戶取消訂閱,以及恢復訂閱(自動續訂蘋果強制要求有恢復按鈕)。

二、內購流程

1.創建商品
內購商品信息.png
內購設置信息.png
內購審核樣式.png
2.App Store Connect????用戶和訪問????沙盒右邊添加沙盒測試人員
沙盒測試賬號.png
3.協議、稅務和銀行業務(需要在測試之前填寫,不然喚起支付的回調里拿不到內購商品信息
協議、稅務和銀行業務.png

協議狀態.png

銀行信息.png

稅務.png
4.代碼調試

步驟如下:

  • 獲取商品列表,從app內讀取或者從自己服務器讀??;
  • 用戶選擇了商品,喚起支付請求,收到購買完成的回調;
  • 購買流程結束后,向服務器發起驗證憑證,以及發放虛擬商品(驗證時開發階段需要用沙盒環境,蘋果推薦使用App Store環境,當返回21007時再進行沙盒環境驗證);
  • 服務端認證分為4步
    • 接受iOS端發來的購買憑證;
    • 判斷憑證是否已存在或驗證過,然后存儲該憑證;
    • 將該憑證發送都蘋果的服務器驗證,并將驗證結果返回給客戶端;(考慮到網絡異常情況,服務器的驗證應該是一個可恢復的隊列,如果網絡失敗了,應該進行重試。 簡單來說就是將該購買憑證用Base64編碼,然后POST給蘋果的驗證服務器,蘋果將驗證結果以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;          // 當前購買類型
@property (nonatomic, copy) void(^Subscribe)(void);     // 內購回調
@property (nonatomic, assign) BOOL verifyVip;           // 是否開始驗證
@property (nonatomic, assign) BOOL isSubscribing;       // 正在進行訂閱操作
@property (nonatomic, assign) BOOL isRestoring;         // 正在進行還原操作

@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;
    //一定要 開啟內購檢測
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    
    [[[UIApplication sharedApplication] keyWindow] addSubview:self.hudProgress];
}

// 恢復方法
- (void)restoreAction {
    //調起蘋果內購恢復接口
    [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:@"不允許程序內付費"];
        });
    }
}

// 收到請求信息
- (void)requestProductData:(NSString *)productID{
    NSLog(@"-------------請求對應的產品信息----------------");
    [self.hudProgress showAnimated:YES];
    self.hudProgress.label.text = @"加載內購信息...";
    
    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;
        }
    }
    
    // 發送購買請求
    if (prod != nil) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.hudProgress.label.text = @"喚起支付...";
        });
        SKPayment *payment = [SKPayment paymentWithProduct:prod];
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
}

// 失敗回調
- (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 = @"";
    });
}

// 監聽購買結果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
    for (SKPaymentTransaction *tran in transactions) {
        switch (tran.transactionState) {
            case SKPaymentTransactionStatePurchased:    // 交易完成
                if (tran.originalTransaction) { // 如果是自動續費的訂單originalTransaction會有內容
                    NSLog(@"自動續費的訂單originalTransaction會有內容");
                } else {  // 普通購買,以及 第一次購買 自動訂閱
                    NSLog(@"普通購買,以及 第一次購買 自動訂閱");
                }
                [self verifyPurchaseWithPaymentTransactionWith:tran];
                
                break;
            case SKPaymentTransactionStatePurchasing: {  //
                dispatch_async(dispatch_get_main_queue(), ^{
                    self.hudProgress.label.text = @"加載中...";
                });
                NSLog(@"商品已經添加進列表");
            }
                break;
                
            case SKPaymentTransactionStateRestored: {
                NSLog(@"已經購買過商品");
                if (!self.verifyVip) {  // 驗證一次,已購商品會有多個,避免重復驗證
                    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:@"恢復失敗"];
    });
}

// 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];
    });
}


/**
 *  驗證購買,避免越獄軟件模擬蘋果請求達到非法購買問題
 *
 */
-(void)verifyPurchaseWithPaymentTransactionWith:(SKPaymentTransaction *)tran{
    WeakSelf
    dispatch_async(dispatch_get_main_queue(), ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            self.hudProgress.label.text = @"驗證內購信息";
        });
    });
    
    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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容