2024 - iOS 内购集成(OC、StoreKit1、不包含本地校验)

1,086 阅读8分钟

使用框架

  1. 【StoreKit】不用多说了
  2. 【MBProgressHUD】购买过程中禁止其他操作、展示购买流程信息
  3. 【SAMKeychain】缓存订单等信息,防止删除app或其他原因导致的数据丢失

流程:

  1. 【初始化】所有代码都封装在一个单例中,app启动时初始化,初始化时会执行检测是否有未完成的交易并处理、开启监听
  2. 【发起购买】[self StartToPayWithProductId:@"" orderID:@"" applicationUsername:@"" completionHandler:^(NSInteger payResult) {}]; ,会检测手机是否开启内购权限,未开启的话会弹框提示
  3. 【购买前检测】检测是否有未完成的交易并处理,无未完成的交易时,开始查询商品信息
  4. 【正式发起购买请求】获取到商品信息,正式发起购买请求,存储交易信息
  5. 【监听购买结果】
  6. 【获取交易凭证】
  7. 【验证交易凭证】
  8. 【交易完成】关闭交易,移除缓存的交易信息

代码如下:

注意点:

  1. #import "AppConfig.h" 是我向服务器验证凭证使用的,无需在意相关代码
  2. - (void)removeAll; 此方法慎用
  3. applicationUsername 由于我这里每次购买都会创建新订单,所以我用订单号拼接,保证这个参数的唯一性,以便于在处理未完成交易时匹配订单号等信息
  4. #define KCM_Key xxx 是我在SAMKeychain缓存的key,最好不要写死,避免在同一设备上登录不同账号 导致的 内购数据错乱
  5. completionHandler 是支付流程结束后 回调支付结果的,以便于进行其他操作
    • 0-未成功发起支付 1-成功 2-取消 3-失败 4-恢复购买 5-其他
  6. 开始购买后,如果有未完成的交易,会结束当前的交易流程:回调值为0 ,然后去处理未完成的交易,未完成的交易处理完后不会自动开始当前的交易
//  KWIAPManager.h
//  Runner
//
//  Created by 渴望 on 2024/1/30.
//
  
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
#import <SAMKeychain/SAMKeychain.h>
#import "MBProgressHUD.h"
#import "AppConfig.h"
  
NS_ASSUME_NONNULL_BEGIN

///最终支付结果回调 0-未成功发起支付 1-成功 2-取消 3-失败 4-恢复购买 5-其他
typedef void (^PaymentCompletionHandler)(NSInteger payResult);

@interface KWIAPManager : NSObject
@property (nonatomic, copy) PaymentCompletionHandler completionHandler;

- (void)callCompletionHandlerWithResult:(NSInteger)payResult;

///初始化
+ (instancetype)sharedInstance;

///开始支付
- (void)StartToPayWithProductId:(NSString *)productId
                        orderID:(NSString *)orderID
            applicationUsername:(NSString *)applicationUsername
              completionHandler:(PaymentCompletionHandler)completionHandler;

///是否有未完成交易
- (void)checkUnFinishedOrders;

///删除所有未完成的交易缓存 慎用! 特殊情况下调用清理
///目前在未发现有未完成交易时也会调用,用于清理交易取消关闭时存储的数据
- (void)removeAll

@property (nonatomic, strong) AppConfig * appconfig; //校验凭证方法类
@end

NS_ASSUME_NONNULL_END

————————————————

//  KWIAPManager.m
//  Runner
//
//  Created by 渴望 on 2024/1/30.
//

// 由于 StoreKit2 仅支持iOS15之后的版本
// 当前仍然使用 StoreKit1

#define KCM_Key [NSString stringWithFormat:@"%@dxjdApplePay",[ZZPUtils readUserDefultsDataForKey:@"UserPhone"]]

#import "KWIAPManager.h"

**@interface** KWIAPManager () <SKPaymentTransactionObserver, SKProductsRequestDelegate>
**@property** (**nonatomic**, **copy**) NSString * productId;
**@property** (**nonatomic**, **copy**) NSString * orderID;
**@property** (**nonatomic**, **copy**) NSString * applicationUsername;
**@property** (**nonatomic**, **copy**) NSString * receipt;

**@property** (**nonatomic**, **strong**) MBProgressHUD * hud;
**@property** (**nonatomic**, **assign**) **BOOL** isRegain; //是否正在恢复未完成的交易
**@property** (**nonatomic**, **assign**) **BOOL** isRecept; //是否是验证未完成的交易
**@end**

- (**void**)callCompletionHandlerWithResult:(NSInteger)payResult {
    **if** (**self**.completionHandler) {
        **self**.completionHandler(payResult);
    }
}

#pragma mark **- 1 单例模式初始化**
**static** KWIAPManager *sharedInstance = **nil**;
+ (**instancetype**)sharedInstance {
    **static** dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[**self** alloc] init];
    });
    **return** sharedInstance;
}

- (**instancetype**)init {
    **self** = [**super** init];
    **if** (**self**) {
        // 初始化操作
        [[SKPaymentQueue defaultQueue] addTransactionObserver:**self**];
        //[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
        **self**.productId = @"";
        **self**.orderID = @"";
        **self**.applicationUsername = @"";
        **self**.receipt = @"";

        NSLog(@"🐯🐯 内购初始化 开启监听");
    }
    **return** **self**;
}

#pragma mark **- 2 发起购买请求**
- (**void**)StartToPayWithProductId:(NSString *)productId
                        orderID:(NSString *)orderID
            applicationUsername:(NSString *)applicationUsername
              completionHandler:(PaymentCompletionHandler)completionHandler {

    **self**.completionHandler = completionHandler;
    **self**.productId = productId;
    **self**.orderID = orderID;
    **self**.applicationUsername = applicationUsername;

    NSLog(@"🐯🐯 点击开始购买");

    **if** ([SKPaymentQueue canMakePayments]) {
        [**self** checkUnFinishedOrders];
    } **else** {
        NSLog(@"🐯🐯 未开启内购权限");
        [**self** callCompletionHandlerWithResult:0];
        [**self** showAlert];
    }
}

#pragma mark **3 处理未完成交易**
- (**void**)checkUnFinishedOrders {
    NSArray * transactions = [SKPaymentQueue defaultQueue].transactions;

    **if** (transactions.count > 0) {
        NSLog(@"🐯🐯 有未完成的交易 %ld 条",transactions.count);
        UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"检测到有未完成的交易" message:@"请继续处理" preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction * action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * **_Nonnull** action) {

            [**self** showMBProgressHUD:@"交易处理中..."];
            **self**.isRegain = **YES**;
            [**self** changeMBProgressHUDMessage:@"检测到有未完成的交易" Detail:@"正在处理..."];
            [**self** dealPayResult:transactions];
            [**self** callCompletionHandlerWithResult:0];
        }];
        [alert addAction:action];
        UIViewController *rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
        [rootViewController presentViewController:alert animated:**YES** completion:**nil**];
        
        **return**;
    }

    //有未验证的交易凭证
    NSMutableArray * payArr = [[NSMutableArray alloc]init];
    NSString * jsonString = [SAMKeychain passwordForService:KCM_Key account:KCM_Key];
    **if** (jsonString.length > 0) {
        NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
        NSError *error;
        payArr = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
        **if** (error) {
            payArr = [[NSMutableArray alloc]init];
        }
    }

    NSMutableArray * dealArr = [[NSMutableArray alloc]init];
    **for** (NSDictionary * dic **in** payArr) {
        NSString * receipt = dic[@"receipt"];
        **if** (receipt != **nil** || receipt != **NULL** || receipt.length > 0) {
            [dealArr addObject:dic];
        }
    }

    **if** (dealArr.count > 0) {
        NSLog(@"🐯🐯 有未验证的交易 %ld 条",transactions.count);
        UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"检测到有未验证的交易" message:@"请继续验证" preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction * action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * **_Nonnull** action) {
            
            [**self** showMBProgressHUD:@"交易处理中..."];
            **self**.isRecept = **YES**;
            [**self** changeMBProgressHUDMessage:@"检测到有未验证的交易" Detail:@"正在验证..."];
            
            NSDictionary * dic = dealArr.firstObject;
            **self**.applicationUsername = dic[@"applicationUsername"];
            **self**.orderID = dic[@"orderID"];
            **self**.productId = dic[@"productId"];
            **self**.receipt = dic[@"receipt"];
            [**self** verifyReceipt:[SKPaymentTransaction new]];
            
            [**self** callCompletionHandlerWithResult:0];
            
        }];
        [alert addAction:action];
        UIViewController *rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
        [rootViewController presentViewController:alert animated:**YES** completion:**nil**];
        
        **return**;
    }

    //会存在交易完成,关闭交易,但是未清理缓存的情况
    //无未关闭的交易信息时,清理缓存内的交易信息
    [**self** removeAll];
    NSLog(@"🐯🐯 无未完成的交易");
    [**self** startProductsRequest];
}

- (**void**)startProductsRequest {
    **self**.isRegain = **NO**;
    //直接检测未完成交易时不会调用
    **if** (**self**.productId.length > 0) {
        [**self** showMBProgressHUD:@"支付中..."];
        NSLog(@"🐯🐯 开始查询商品信息");
        NSSet *set = [NSSet setWithArray:@[**self**.productId]];
        SKProductsRequest *productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
        productRequest.delegate = **self**;
        [productRequest start];
    }
}

#pragma mark **- 4 查询商品信息回调**
- (**void**)productsRequest:(**nonnull** SKProductsRequest *)request didReceiveResponse:(**nonnull** SKProductsResponse *)response {
    NSArray *product = response.products;
    **if** (product.count == 0) {
        NSLog(@"🐯🐯 无法获取商品信息,请重试");
        [**self** hideMBProgressHUD:@"支付失败" Detail:@"无法获取商品信息,请重试"];
        [**self** callCompletionHandlerWithResult:0];
    } **else** {
        NSLog(@"🐯🐯 发起购买请求");
        SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product[0]];
        payment.applicationUsername = **self**.applicationUsername;
        payment.productIdentifier = **self**.productId;
        
        //存储交易信息
        NSDictionary * dic = @{@"applicationUsername":**self**.applicationUsername,
                               @"orderID": **self**.orderID,
                               @"productId": **self**.productId,
                               @"receipt": @""
        };
        [**self** save:dic];

        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
}


#pragma mark **5 购买结果回调**
- (**void**)paymentQueue:(**nonnull** SKPaymentQueue *)queue updatedTransactions:(**nonnull** NSArray<SKPaymentTransaction *> *)transactions {
    [**self** dealPayResult:transactions];
}

- (**void**)dealPayResult:(NSArray<SKPaymentTransaction *>*)transactions{
    **for** (SKPaymentTransaction * tran **in** transactions){
        
        **switch** (tran.transactionState) {
            **case** SKPaymentTransactionStatePurchasing:
            {
                NSLog(@"🐯🐯 正在支付");
            }
                **break**;
            **case** SKPaymentTransactionStatePurchased:
            {
                NSLog(@"🐯🐯 支付完成");
                [**self** getReceipt:tran];
            }
                **break**;
            **case** SKPaymentTransactionStateRestored:
            {
                NSLog(@"🐯🐯 已经购买过商品,恢复购买");
                [**self** finish:tran];
            }
                **break**;
            **case** SKPaymentTransactionStateFailed:
            {
                NSLog(@"🐯🐯 支付失败%@",tran.error);
                [**self** finish:tran];
            }
                **break**;
            **default**:
            {
                NSLog(@"🐯🐯 --");
                [**self** finish:tran];
            }
                **break**;
        }
    }
}

#pragma mark **6 获取交易凭证**
- (**void**)getReceipt:(SKPaymentTransaction *)tran {

    NSLog(@"🐯🐯 开始获取交易凭证");
    NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
    **if** ([[NSFileManager defaultManager] fileExistsAtPath:[receiptUrl path]]) {
        NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
        **self**.receipt = [receiptData base64EncodedStringWithOptions:0];
        NSLog(@"🐯🐯 交易凭证:%@",**self**.receipt);

        //存储交易信息
        NSDictionary * dic = @{@"applicationUsername":**self**.applicationUsername,
                               @"orderID": **self**.orderID,
                               @"productId": **self**.productId,
                               @"receipt": **self**.receipt
        };
        [**self** save:dic];

        //验证票据
        [**self** verifyReceipt:tran];
    } **else** {
        NSLog(@"Receipt request done but there is no receipt");
    }
}

#pragma mark **- 7 验证票据**
- (**void**)verifyReceipt:(SKPaymentTransaction *)tran{

    **if** (**self**.isRegain) {
        NSLog(@"🐯🐯 处理未完成的交易时 恢复订单号等信息");
        NSMutableArray * payArr = [[NSMutableArray alloc]init];
        NSString * jsonString = [SAMKeychain passwordForService:KCM_Key account:KCM_Key];
        **if** (jsonString.length > 0) {
            NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
            NSError *error;
            payArr = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
            **if** (error) {
                payArr = [[NSMutableArray alloc]init];
            }
        }

        **if** (payArr.count > 0) {
            **for** (NSDictionary * paydic **in** payArr) {
                **if** ([paydic[@"applicationUsername"] isEqualToString:tran.payment.applicationUsername]) {
                    **self**.productId = paydic[@"productId"];
                    **self**.orderID = paydic[@"orderID"];
                    **self**.applicationUsername = paydic[@"applicationUsername"];
                    
                    **break**;
                }
            }
        }
    }
    
    NSLog(@"🐯🐯 开始验证票据");
    [**self** changeMBProgressHUDMessage:@"支付成功" Detail:@"正在验证交易内容..."];
   
    
    //测试验证失败
    //     NSLog(@"🐯🐯 验证失败 %@",KCM_Key);
    //     [self hideMBProgressHUD:@"验证失败" Detail:@""];
    //     [self callCompletionHandlerWithResult:5];
    
   
    [**self**.appconfig verifyReceipt:**self**.receipt orderId:**self**.orderID resultHandler:^(**BOOL** result) {
        **if** (result) {
            
            NSLog(@"🐯🐯 验证成功");
            [**self** hideMBProgressHUD:@"验证成功" Detail:@""];
            [**self** callCompletionHandlerWithResult:1];
            
            //关闭交易
            [**self** finish:tran];
        }**else**{
            
            NSLog(@"🐯🐯 验证失败 %@",KCM_Key);
            [**self** hideMBProgressHUD:@"验证失败" Detail:@""];
            [**self** callCompletionHandlerWithResult:5];
            
        }
    }];
    
}

#pragma mark **- 8 关闭当前交易**
- (**void**)finish:(SKPaymentTransaction *)tran {
    NSLog(@"🐯🐯 关闭交易");
    
    **if** (**self**.isRecept || **self**.isRegain) {
        UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"温馨提示" message:@"为确保您的购买内容准确发放,\n请重启应用!" preferredStyle:UIAlertControllerStyleAlert];
        
        UIAlertAction * action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * **_Nonnull** action) {
            
        }];
        [alert addAction:action];
        
        UIViewController *rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
        [rootViewController presentViewController:alert animated:**YES** completion:**nil**];
    }
    
    //删除缓存
    [**self** remove:**self**.applicationUsername];
    
    //重置数据
    **self**.productId = @"";
    **self**.orderID = @"";
    **self**.applicationUsername = @"";
    **self**.receipt = @"";
    **self**.isRegain = **NO**;
    **self**.isRecept = **NO**;
    
    NSLog(@"%ld",[SKPaymentQueue defaultQueue].transactions.count);
    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
    
    **if** (tran.transactionState == SKPaymentTransactionStatePurchased) {
        //支付完成状态
    } **else** **if** (tran.transactionState == SKPaymentTransactionStateRestored) {
        [**self** hideMBProgressHUD:@"已恢复购买" Detail:@""];
        [**self** callCompletionHandlerWithResult:4];
    } **else** **if** (tran.transactionState == SKPaymentTransactionStateFailed) {
        **if** (tran.error.code == SKErrorPaymentCancelled) {
            [**self** hideMBProgressHUD:@"已取消支付" Detail:@""];
            [**self** callCompletionHandlerWithResult:2];
        } **else** {
            [**self** hideMBProgressHUD:@"支付失败" Detail:tran.error.localizedDescription];
            [**self** callCompletionHandlerWithResult:3];
        }
    } **else** {
        [**self** hideMBProgressHUD:@"" Detail:@""];
        [**self** callCompletionHandlerWithResult:5];
    }
}

#pragma mark **- 存储交易信息**
- (**void**)save:(NSDictionary *)dic{
    
    NSMutableArray * payArr = [[NSMutableArray alloc]init];
    NSString * jsonString = [SAMKeychain passwordForService:KCM_Key account:KCM_Key];
    **if** (jsonString.length > 0) {
        NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
        NSError *error;
        payArr = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
        **if** (error) {
            payArr = [[NSMutableArray alloc]init];
        }
    }
    
    NSLog(@"🐯🐯 存储交易信息 原:%ld条",payArr.count);
  
    NSMutableArray *itemsToRemove = [NSMutableArray array];
    **for** (NSDictionary * paydic **in** payArr) {
        **if** ([paydic[@"applicationUsername"] isEqualToString:dic[@"applicationUsername"]]) {
            [itemsToRemove addObject:paydic];
        }
    }
    [payArr removeObjectsInArray:itemsToRemove];
    [payArr addObject:dic];
    NSLog(@"🐯🐯 存储交易信息 现:%ld条",payArr.count);
    
    NSError *error1;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payArr options:NSJSONWritingPrettyPrinted error:&error1];
    NSString *jsonString1 = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
    [SAMKeychain setPassword:jsonString1 forService:KCM_Key account:KCM_Key];
    
}

#pragma mark **- 删除交易信息**
- (**void**)remove:(NSString *)applicationUsername {
    
    NSMutableArray * payArr = [[NSMutableArray alloc]init];
    NSString * jsonString = [SAMKeychain passwordForService:KCM_Key account:KCM_Key];
    **if** (jsonString.length > 0) {
        NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
        NSError *error;
        payArr = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
        **if** (error) {
            payArr = [[NSMutableArray alloc]init];
        }
    }

    NSLog(@"🐯🐯 删除交易信息 原:%ld条",payArr.count);
    **for** (NSDictionary * paydic **in** payArr) {
        **if** ([paydic[@"applicationUsername"] isEqualToString:applicationUsername]) {
            [payArr removeObject:paydic];
        }
    }
    NSLog(@"🐯🐯 删除交易信息 现:%ld条",payArr.count);
    NSError *error1;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payArr options:NSJSONWritingPrettyPrinted error:&error1];
    NSString *jsonString1 = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
    [SAMKeychain setPassword:jsonString1 forService:KCM_Key account:KCM_Key];
}

#pragma mark **- 删除所有交易信息**
- (**void**)removeAll {
    [SAMKeychain setPassword:@"" forService:KCM_Key account:KCM_Key];
}

#pragma mark **- 加载框**
- (**void**)showMBProgressHUD:(NSString *)text {
    dispatch_async(dispatch_get_main_queue(), ^{
        // 在主线程上访问 MBProgressHUD
        **self**.hud = [MBProgressHUD showHUDAddedTo:[UIApplication sharedApplication].keyWindow animated:**YES**];
        **self**.hud.mode = MBProgressHUDModeIndeterminate;
        **self**.hud.labelText = text; //
        **self**.hud.detailsLabelText = @"";
        **self**.hud.labelFont = [UIFont systemFontOfSize:15 weight:UIFontWeightBold];
        **self**.hud.labelFont = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
        [**self**.hud show:**YES**];
    });
}

- (**void**)changeMBProgressHUDMessage:(NSString *)message Detail:(NSString *)detail {
    dispatch_async(dispatch_get_main_queue(), ^{
        **self**.hud.labelText = message;
        **self**.hud.detailsLabelText = detail;
    });
}

- (**void**)hideMBProgressHUD:(NSString *)message Detail:(NSString *)detail {
    dispatch_async(dispatch_get_main_queue(), ^{
        **if** (message.length == 0 && detail.length == 0) {
            [MBProgressHUD hideAllHUDsForView:[UIApplication sharedApplication].keyWindow animated:**YES**];
            //[self.hud hide:YES];
        }**else**{
            [MBProgressHUD hideAllHUDsForView:[UIApplication sharedApplication].keyWindow animated:**YES**];
            //[self.hud hide:YES];
            **self**.hud = [MBProgressHUD showHUDAddedTo:[UIApplication sharedApplication].keyWindow animated:**YES**];
            **self**.hud.mode = MBProgressHUDModeText;
            **self**.hud.labelText = message;
            **self**.hud.detailsLabelText = detail;
            [**self**.hud hide:**YES** afterDelay:2];
        }
    });
}

#pragma mark **- 权限Alert**
- (**void**)showAlert {
    UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"温馨提示" message:@"您未开启内购权限\n开启方法:在【手机设置】中搜索【内容与隐私限制】,点击【iTunes Store与App Store购买项目】,将【App内购买项目】设置为【允许】" preferredStyle:UIAlertControllerStyleAlert];
    
    UIAlertAction * action = [UIAlertAction actionWithTitle:@"我知道了" style:UIAlertActionStyleDefault handler:^(UIAlertAction * **_Nonnull** action) {
        
    }];
    [alert addAction:action];
    
    UIViewController *rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
    [rootViewController presentViewController:alert animated:**YES** completion:**nil**];

}

**@end**