iOS内购订阅相关代码(可直接使用)

一、内购支付的流程
用户选择需要订阅的商品,发起购买,支付完成,校验票据。
具体步骤:
1.新建内购相关文件
.h代码

//
//  JPurchaseManager.h
//  CameraProduct
//
//  Created by lier on 2024/9/6.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface JPurchaseManager : NSObject

@property(nonatomic,strong)NSMutableDictionary *__nullable track;

+(instancetype)shareManager;
// 购买
-(void)purchaseWithProductIdentifier:(NSString *)identifier;
//恢复购买
-(void)resumePurchase;

@end

NS_ASSUME_NONNULL_END

.m代码

//
//  JPurchaseManager.m
//  CameraProduct
//
//  Created by lier on 2024/9/6.
//

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

@interface JPurchaseManager ()<SKProductsRequestDelegate,SKPaymentTransactionObserver>

@property(nonatomic,strong)SKProductsRequest *__nullable productsRequest;
@property(nonatomic,copy)NSString *productIdentify;
@property(nonatomic,strong)SKPaymentTransaction *refreshTrans;
@property(nonatomic,assign)BOOL isShowLoading;
@property(nonatomic,assign)BOOL isRestore;

@end

@implementation JPurchaseManager

-(void)dealloc {
    [self releaseRequest];
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

- (void)releaseRequest {
    if(_productsRequest) {
        [_productsRequest cancel];
        _productsRequest.delegate = nil;
        _productsRequest = nil;
    }
}

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

-(instancetype)init {
    self = [super init];
    if (self) {
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
    return self;
}
//购买
-(void)purchaseWithProductIdentifier:(NSString *)identifier {
    self.isShowLoading = YES;
    _productIdentify = identifier;
    NSArray *transactions = [SKPaymentQueue defaultQueue].transactions;
    if (transactions.count > 0) {
        for (SKPaymentTransaction *transaction in transactions) {
            if (transaction.transactionState == SKPaymentTransactionStatePurchased
                || transaction.transactionState == SKPaymentTransactionStateRestored) {
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            }
        }
    }
    
    if (identifier.length > 0) {
        if ([SKPaymentQueue canMakePayments]) {
            [SVProgressHUD showWithStatus:@"正在添加商品"];
            [self releaseRequest];
            NSSet *productIdentifiers = [NSSet setWithObjects:identifier, nil];
            self.productsRequest = [[SKProductsRequest alloc]initWithProductIdentifiers:productIdentifiers];
            self.productsRequest.delegate = self;
            [self.productsRequest start];
        } else {
            _productIdentify = nil;
            self.isShowLoading = NO;
            [SVProgressHUD showErrorWithStatus:@"尚未开启应用内付费购买"];
        }
    } else {
        _productIdentify = nil;
        self.isShowLoading = NO;
        [SVProgressHUD showErrorWithStatus:@"无效商品"];
    }
}
//恢复购买
- (void)resumePurchase {
    self.isRestore = YES;
    self.isShowLoading = YES;
    _productIdentify = nil;
    self.track = nil;
    [SVProgressHUD showWithStatus:@"恢复购买中..."];
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}

#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    NSArray * arrProducts = response.products;
    
    if ([arrProducts count] == 0) {
        _productIdentify = nil;
        self.isShowLoading = NO;
        [SVProgressHUD showErrorWithStatus:@"没有对应的可订阅项目"];
        
        return;
    }
    
    SKProduct *currentPoduct = nil;
    for(SKProduct *product in arrProducts) {
        if ([self.productIdentify isEqualToString:product.productIdentifier]) {
            currentPoduct = product;
        }
    }
    
    if (currentPoduct != nil) {
        SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:currentPoduct];
        payment.quantity = 1;
        payment.applicationUsername = [JScreenUnit manager].device_id;
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    } else {
        _productIdentify = nil;
        self.isShowLoading = NO;
        [SVProgressHUD showErrorWithStatus:@"没有对应的可订阅项目"];
    }
}

- (void)request:(SKRequest*)request didFailWithError:(NSError*)error {
    _productIdentify = nil;
    self.isShowLoading = NO;
    [SVProgressHUD showErrorWithStatus:@"操作失败"];
}

#pragma mark - SKPaymentTransactionObserver
- (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product {
    return YES;
}

/** 监听购买结果 */
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
//    NSLog(@"=== updatedTransactions ===");
    if (!self.isRestore) {
        for (int a = 0 ; a < transactions.count; a++) {
            SKPaymentTransaction *transaction = [transactions objectAtIndex:a];
//            NSLog(@"updatedTransactions ===%@",transaction);
            switch (transaction.transactionState) {
                case SKPaymentTransactionStatePurchasing: {
                    [SVProgressHUD showWithStatus:@"正在处理"];
                }
                    break;
                case SKPaymentTransactionStateFailed: {
                    //交易失败
                    if (transaction.error.code == SKErrorPaymentCancelled) {
                        self.isShowLoading = NO;
                        [SVProgressHUD showInfoWithStatus:@"您取消了支付"];
                        self.productIdentify = nil;
                    } else {
                        self.isShowLoading = NO;
                        [SVProgressHUD showErrorWithStatus:@"交易失败"];
                    }
                    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                }
                    break;
                case SKPaymentTransactionStatePurchased: {
                    NSString *ID = @"";
                    if (![NSString isBlank:transaction.originalTransaction.transactionIdentifier]) {
                        ID = transaction.originalTransaction.transactionIdentifier;
                    } else if (![NSString isBlank:transaction.transactionIdentifier]) {
                        ID = transaction.transactionIdentifier;
                    }
                    [self verifyTicketsWithTransation:transaction transIds:@[ID]];
                }
                    break;
                case SKPaymentTransactionStateDeferred: {
                    //等待,不做任何处理
                }
                    break;
                default:
                    break;
            }
        }
    } else {
        for (int a = 0 ; a < transactions.count; a++) {
            SKPaymentTransaction *transaction = [transactions objectAtIndex:a];
//            NSLog(@"+++ updatedTransactions ===%@===transactionState %ld",transaction.transactionIdentifier,transaction.transactionState);
            switch (transaction.transactionState) {
                case SKPaymentTransactionStateRestored: {
                    //已经购买过该商品
                    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                }
                    break;
                default:
                    break;
            }
        }
    }
}

- (void)paymentQueue:(SKPaymentQueue*)queue restoreCompletedTransactionsFailedWithError:(NSError*)error {
    //在将交易从用户的购买历史记录添加回队列时遇到错误时
    self.isShowLoading = NO;
    [SVProgressHUD showErrorWithStatus:@"恢复购买失败"];
}

- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue*)queue {
    //当用户的购买历史记录中的所有交易成功添加回队列时
    if (queue.transactions.count == 0) {
        self.isShowLoading = NO;
        self.isRestore = NO;
        [SVProgressHUD showErrorWithStatus:@"无订阅记录,现在去订阅"];
    } else {
        //验证票据一下
        NSArray *arrIDs = [self getoriginalTransIds:queue.transactions];
        
        [self verifyTicketsWithTransation:nil transIds:arrIDs];
    }
}

#pragma mark - 验证购买
- (void)verifyTicketsWithTransation:(SKPaymentTransaction *__nullable)transaction transIds:(NSArray *)transIds {
    // 从沙盒中获取交易凭证(收据)
    NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
    // 转化为base64字符串
    NSString *receipt = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    
    if (receipt.length > 0) {
        if (self.isShowLoading) {
            [SVProgressHUD showWithStatus:@"凭证验证中..."];
        }
        NSMutableDictionary *para = [NSMutableDictionary dictionary];
        if (transIds.count > 0) {
            [para setValue:transIds forKey:@"transaction_ids"];
        }
        
        [para setValue:@(self.isRestore) forKey:@"restore_of"];
        [para setObject:receipt forKey:@"receipt"];
        
        if (_track && ![NSString isBlank:self.productIdentify]) {
            self.productIdentify = nil;
            [_track setValue:[NSString getCurrentTimeInterval] forKey:@"action_time"];
            [para setValue:_track forKey:@"track"];
        }
        MJWeakSelf
        [JRequestHTTPEngine requestWithURL:@"校验票据接口" withMethod:postMethod params:para successBlock:^(BOOL isSuccess, NSDictionary * _Nonnull result) {
            weakSelf.track = nil;
            if (transaction) {
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            }
            if (isSuccess) {
                NSDictionary *dataDic = [result objectForKey:@"data"];
                BOOL vip = [[dataDic objectForKey:@"vip"] boolValue];
                if (vip) {
                    [JScreenUnit manager].mineInfo.identity.vip = YES;
                    kPostNotification(kMembershipStatusChangeNotify, nil);
                    kPostNotification(kPurchaseSuccessCloseNotify, nil);
                    if (weakSelf.isShowLoading) {
                        weakSelf.isShowLoading = NO;
                        if (!weakSelf.isRestore) {
                            [SVProgressHUD showSuccessWithStatus:@"交易成功"];
                        } else {
                            [SVProgressHUD showSuccessWithStatus:@"恢复成功"];
                        }
                    }
                } else {
                    if (weakSelf.isShowLoading) {
                        weakSelf.isShowLoading = NO;
                        if (!weakSelf.isRestore) {
                            [SVProgressHUD showErrorWithStatus:@"交易服务中断了,请联系我们~"];
                        } else {
                            [SVProgressHUD showErrorWithStatus:@"会员已过期,请重新订阅"];
                        }
                    }
                }
            } else {
                if (weakSelf.isShowLoading) {
                    weakSelf.isShowLoading = NO;
                    [SVProgressHUD showErrorWithStatus:result[@"message"]];
                }
            }
            weakSelf.isRestore = NO;
        } failureBlock:^(NSString * _Nonnull errorString) {
            if (weakSelf.isShowLoading) {
                weakSelf.isShowLoading = NO;
                [SVProgressHUD showErrorWithStatus:errorString];
            }
            weakSelf.isRestore = NO;
        }];
    } else {
        self.isRestore = NO;
        self.productIdentify = nil;
        if (self.isShowLoading) {
            self.isShowLoading = NO;
            [SVProgressHUD dismiss];
        }
    }
}

- (NSArray *)getoriginalTransIds:(NSArray *)transArr {
    //遍历取得原始订单号,按时间正序去重加入数组
    NSMutableDictionary *originalTransDic = @{}.mutableCopy;
    for (SKPaymentTransaction *trans in transArr) {
        NSString *tid = trans.transactionIdentifier;
        NSString *timestamp = [NSString stringWithFormat:@"%ld", (NSInteger)[trans.transactionDate timeIntervalSince1970]];
        if (trans.originalTransaction) {
            tid = trans.originalTransaction.transactionIdentifier;
            timestamp = [NSString stringWithFormat:@"%ld", (NSInteger)[trans.originalTransaction.transactionDate timeIntervalSince1970]];
        }
        if (![NSString isBlank:tid] && ![NSString isBlank:timestamp]) {
            [originalTransDic setValue:tid forKey:timestamp];
        }
    }
    //按时间升序
    NSArray *keysArr = [originalTransDic.allKeys sortedArrayUsingComparator:^NSComparisonResult(NSString *  _Nonnull obj1, NSString *  _Nonnull obj2) {
        //升序
        NSComparisonResult result = [obj1 compare:obj2];
        return result;
    }];
    //NSSet会去重
    NSMutableSet *mutSet = [NSMutableSet set];
    for (NSString *key in keysArr) {
        if ([originalTransDic objectForKey:key]) {
            [mutSet addObject:originalTransDic[key]];
        }
    }
    return [mutSet allObjects];
}

@end
注意:

1.之所以吧SKPaymentTransactionStateRestored这个状态放在else进行操作,是为了避免校验票据的时候会走多次的问题。可以把

case SKPaymentTransactionStateRestored: {
                    //已经购买过该商品
                    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                }
                    break;

这句话放到SKPaymentTransactionStateDeferred之后进行打印操作试试。避免服务器内存瞬间暴涨导致崩溃。
2.校验票据接口里面用到的通知kPostNotification是为了操作一些业务。比如刷新用户VIP状态、购买成功后当前订阅界面关闭掉等业务,其他自行操作。
3.isShowLoading是为了是不是显示加载框。避免用户乱操作。
4.device_id使用下面方法获取:

- (NSString *)device_id {
    if (!_device_id) {
        NSString *locallyID = [kUserDefaults valueForKey:kDeviceKey];
        BOOL locally = [NSString isBlank:locallyID];
        
        NSString *keyChainID = [JScreenUnit readData:deviceIDChain];
        BOOL keyChain = [NSString isBlank:keyChainID];
        
        if (locally && keyChain) {
            CFUUIDRef puuid = CFUUIDCreate(nil);
            CFStringRef uuidString = CFUUIDCreateString(nil, puuid);
            NSString *result = (NSString *)CFBridgingRelease(CFStringCreateCopy(NULL, uuidString));
            NSMutableString *tmpResult = result.mutableCopy;
            _device_id = tmpResult;
        } else if (!locally) {
            _device_id = locallyID;
        } else if (!keyChain) {
            _device_id = keyChainID;
        }
        if (locally) {
            [kUserDefaults setValue:_device_id forKey:kDeviceKey];
            [kUserDefaults synchronize];
        }
        if (keyChain) {
            [JScreenUnit saveData:_device_id withIdentifier:deviceIDChain];
        }
    }
    return _device_id;
}

/*!
 保存数据
 */
+ (BOOL)saveData:(id)data withIdentifier:(NSString*)identifier {
    // 获取存储的数据的条件
    NSMutableDictionary * saveQueryMutableDictionary = [self keyChainIdentifier:identifier];
    // 删除旧的数据
    SecItemDelete((CFDictionaryRef)saveQueryMutableDictionary);
    // 设置新的数据
    [saveQueryMutableDictionary setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
    // 添加数据
    OSStatus saveState = SecItemAdd((CFDictionaryRef)saveQueryMutableDictionary, nil);
    // 释放对象
    saveQueryMutableDictionary = nil ;
    // 判断是否存储成功
    if (saveState == errSecSuccess) {
        return YES;
    }
    return NO;
}
/*!
 读取数据
 */
+ (id)readData:(NSString*)identifier {
    id idObject = nil ;
    // 通过标记获取数据查询条件
    NSMutableDictionary * keyChainReadQueryMutableDictionary = [self keyChainIdentifier:identifier];
    // 这是获取数据的时,必须提供的两个属性
    // TODO: 查询结果返回到 kSecValueData
    [keyChainReadQueryMutableDictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
    // TODO: 只返回搜索到的第一条数据
    [keyChainReadQueryMutableDictionary setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
    // 创建一个数据对象
    CFDataRef keyChainData = nil ;
    //NSError  *error;
    // 通过条件查询数据
    if (SecItemCopyMatching((CFDictionaryRef)keyChainReadQueryMutableDictionary , (CFTypeRef *)&keyChainData) == noErr){
        @try {
            idObject = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)(keyChainData)];
        } @catch (NSException * exception){
            NSLog(@"Unarchive of search data where %@ failed of %@ ",identifier,exception);
        }
    }
    if (keyChainData) {
        CFRelease(keyChainData);
    }
    // 释放对象
    keyChainReadQueryMutableDictionary = nil;
    // 返回数据
    return idObject ;
}

5.使用:

#pragma mark - 确认
-(void)comeButtonClick{
    if (!self.chooseBtn.selected) {
        [SVProgressHUD showInfoWithStatus:@"请阅读并同意《隐私协议》和《服务条款》"];
        return;
    }
    //购买
    [[JPurchaseManager shareManager] purchaseWithProductIdentifier:[self.model.products firstObject].pID];
}

7.恢复购买

//恢复购买
- (void)reBuyClick {
    [[JPurchaseManager shareManager] resumePurchase];
}

6.记得释放。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 一、介绍 iOS 的 App 内购类型有四种:消耗型商品:只可使用一次的产品,使用之后即失效,必须再次购买。示例:...
    凡几多阅读 106,848评论 120 180
  • 成熟的小朋友要学会自己看文档置于为什么示例代码是swift,因为官方文档没有给OC版本的,我也懒得改。更新-后续的...
    FireStroy阅读 6,397评论 1 12
  • 一般来说,开发人员刚接触内购,都会遇到流程不清楚、千头万绪。如何一次性搞定内购问题? 一、掌握内购流程: 1、完成...
    little_ma阅读 53,521评论 80 145
  • 一、有关Apple内购 1. SKStorefront:包含App Store店面位置和唯一标识符的对象。 您可以...
    雨泽Sunshine阅读 2,342评论 0 4
  • 最近有个项目客户总是反应掉单,于是乎就看了看内购相关的东西,发现坑还真是不少,这里做个总结。 IAP,即in-Ap...
    糖炒0栗子阅读 17,958评论 0 22