一、內(nèi)購支付的流程
用戶選擇需要訂閱的商品芬位,發(fā)起購買无拗,支付完成,校驗票據(jù)晶衷。
具體步驟:
1.新建內(nèi)購相關(guān)文件
.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;
//恢復(fù)購買
-(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:@"尚未開啟應(yīng)用內(nèi)付費(fèi)購買"];
}
} else {
_productIdentify = nil;
self.isShowLoading = NO;
[SVProgressHUD showErrorWithStatus:@"無效商品"];
}
}
//恢復(fù)購買
- (void)resumePurchase {
self.isRestore = YES;
self.isShowLoading = YES;
_productIdentify = nil;
self.track = nil;
[SVProgressHUD showWithStatus:@"恢復(fù)購買中..."];
[[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:@"沒有對應(yīng)的可訂閱項目"];
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:@"沒有對應(yīng)的可訂閱項目"];
}
}
- (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;
}
/** 監(jiān)聽購買結(jié)果 */
- (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: {
//已經(jīng)購買過該商品
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
break;
default:
break;
}
}
}
}
- (void)paymentQueue:(SKPaymentQueue*)queue restoreCompletedTransactionsFailedWithError:(NSError*)error {
//在將交易從用戶的購買歷史記錄添加回隊列時遇到錯誤時
self.isShowLoading = NO;
[SVProgressHUD showErrorWithStatus:@"恢復(fù)購買失敗"];
}
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue*)queue {
//當(dāng)用戶的購買歷史記錄中的所有交易成功添加回隊列時
if (queue.transactions.count == 0) {
self.isShowLoading = NO;
self.isRestore = NO;
[SVProgressHUD showErrorWithStatus:@"無訂閱記錄,現(xiàn)在去訂閱"];
} else {
//驗證票據(jù)一下
NSArray *arrIDs = [self getoriginalTransIds:queue.transactions];
[self verifyTicketsWithTransation:nil transIds:arrIDs];
}
}
#pragma mark - 驗證購買
- (void)verifyTicketsWithTransation:(SKPaymentTransaction *__nullable)transaction transIds:(NSArray *)transIds {
// 從沙盒中獲取交易憑證(收據(jù))
NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
// 轉(zhuǎn)化為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:@"校驗票據(jù)接口" 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:@"恢復(fù)成功"];
}
}
} else {
if (weakSelf.isShowLoading) {
weakSelf.isShowLoading = NO;
if (!weakSelf.isRestore) {
[SVProgressHUD showErrorWithStatus:@"交易服務(wù)中斷了晌纫,請聯(lián)系我們~"];
} 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 {
//遍歷取得原始訂單號,按時間正序去重加入數(shù)組
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這個狀態(tài)放在else進(jìn)行操作锹漱,是為了避免校驗票據(jù)的時候會走多次的問題箭养。可以把
case SKPaymentTransactionStateRestored: {
//已經(jīng)購買過該商品
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
break;
這句話放到SKPaymentTransactionStateDeferred
之后進(jìn)行打印操作試試哥牍。避免服務(wù)器內(nèi)存瞬間暴漲導(dǎo)致崩潰毕泌。
2.校驗票據(jù)接口里面用到的通知kPostNotification
是為了操作一些業(yè)務(wù)。比如刷新用戶VIP狀態(tài)嗅辣、購買成功后當(dāng)前訂閱界面關(guān)閉掉等業(yè)務(wù)撼泛,其他自行操作。
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;
}
/*!
保存數(shù)據(jù)
*/
+ (BOOL)saveData:(id)data withIdentifier:(NSString*)identifier {
// 獲取存儲的數(shù)據(jù)的條件
NSMutableDictionary * saveQueryMutableDictionary = [self keyChainIdentifier:identifier];
// 刪除舊的數(shù)據(jù)
SecItemDelete((CFDictionaryRef)saveQueryMutableDictionary);
// 設(shè)置新的數(shù)據(jù)
[saveQueryMutableDictionary setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
// 添加數(shù)據(jù)
OSStatus saveState = SecItemAdd((CFDictionaryRef)saveQueryMutableDictionary, nil);
// 釋放對象
saveQueryMutableDictionary = nil ;
// 判斷是否存儲成功
if (saveState == errSecSuccess) {
return YES;
}
return NO;
}
/*!
讀取數(shù)據(jù)
*/
+ (id)readData:(NSString*)identifier {
id idObject = nil ;
// 通過標(biāo)記獲取數(shù)據(jù)查詢條件
NSMutableDictionary * keyChainReadQueryMutableDictionary = [self keyChainIdentifier:identifier];
// 這是獲取數(shù)據(jù)的時,必須提供的兩個屬性
// TODO: 查詢結(jié)果返回到 kSecValueData
[keyChainReadQueryMutableDictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
// TODO: 只返回搜索到的第一條數(shù)據(jù)
[keyChainReadQueryMutableDictionary setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
// 創(chuàng)建一個數(shù)據(jù)對象
CFDataRef keyChainData = nil ;
//NSError *error;
// 通過條件查詢數(shù)據(jù)
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;
// 返回數(shù)據(jù)
return idObject ;
}
5.使用:
#pragma mark - 確認(rèn)
-(void)comeButtonClick{
if (!self.chooseBtn.selected) {
[SVProgressHUD showInfoWithStatus:@"請閱讀并同意《隱私協(xié)議》和《服務(wù)條款》"];
return;
}
//購買
[[JPurchaseManager shareManager] purchaseWithProductIdentifier:[self.model.products firstObject].pID];
}
7.恢復(fù)購買
//恢復(fù)購買
- (void)reBuyClick {
[[JPurchaseManager shareManager] resumePurchase];
}
6.記得釋放。