一、内购支付的流程
用户选择需要订阅的商品,发起购买,支付完成,校验票据。
具体步骤:
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.记得释放。