MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的StoreKit内购流程实现

2023-08-022.0k 阅读

一、StoreKit 简介

在 iOS 应用开发中,内购(In-App Purchase)是一种重要的盈利模式,允许用户在应用内购买虚拟商品、订阅服务或解锁高级功能等。StoreKit 框架为开发者提供了实现内购功能的接口,它无缝集成了苹果的 App Store 支付系统,确保支付过程的安全与便捷。

1.1 框架组成

StoreKit 框架包含多个关键类,如 SKProductSKPaymentSKPaymentQueue 等。SKProduct 代表应用内可购买的商品,包含商品的标识符、价格、描述等信息。SKPayment 用于创建支付请求,开发者通过它指定要购买的商品。SKPaymentQueue 则是管理支付队列的类,负责处理支付请求的发送、接收支付结果等操作。

1.2 应用场景

内购的应用场景丰富多样。例如,在游戏应用中,用户可以购买虚拟货币、道具或解锁新关卡;在内容类应用中,用户能够订阅高级内容,如无广告版本、独家文章或视频等。通过合理设计内购功能,开发者可以为用户提供增值服务,同时实现应用的商业价值。

二、准备工作

在开始实现内购流程前,需要完成一系列准备工作。

2.1 配置 App Store Connect

  1. 创建 App 内购买项目:登录 App Store Connect,进入对应的应用页面。在 “功能” 中选择 “App 内购买项目”,点击 “创建”。根据内购类型(消耗型、非消耗型、自动续期订阅等)填写详细信息,包括产品 ID、价格、描述等。产品 ID 是唯一标识内购商品的字符串,在代码中会用于识别商品,务必保证其准确性和唯一性。
  2. 设置定价和可用性:为内购项目选择合适的价格层级,并确定其在哪些地区可用。苹果提供了多种价格层级供开发者选择,需综合考虑目标用户群体、市场定位和盈利预期来确定合适的价格。
  3. 准备审核素材:如果内购项目需要审核,如涉及新的功能或内容,需要准备相关的审核素材,如截图、视频演示等,以帮助审核人员理解内购功能的用途和价值。

2.2 配置 Xcode 项目

  1. 导入 StoreKit 框架:在 Xcode 项目导航器中,选择项目文件,点击 “Build Phases” 标签。展开 “Link Binary With Libraries”,点击 “+” 按钮,搜索并添加 “StoreKit.framework”。这样就将 StoreKit 框架集成到项目中,使代码能够调用框架内的类和方法。
  2. 配置权限:确保项目的 Info.plist 文件中配置了正确的权限。虽然 StoreKit 本身不需要额外的特殊权限,但正确的配置可以避免潜在的问题。例如,可以添加 NSAppTransportSecurity 配置,以确保应用在使用网络进行支付时遵循苹果的安全传输要求。

三、加载商品信息

加载商品信息是内购流程的第一步,通过获取商品信息,应用能够展示可购买的商品列表给用户。

3.1 创建产品请求

在 Objective-C 中,使用 SKProductsRequest 类来请求商品信息。以下是创建产品请求的代码示例:

#import <StoreKit/StoreKit.h>

@interface YourViewController () <SKProductsRequestDelegate>

@property (nonatomic, strong) NSMutableArray <SKProduct *> *products;

@end

@implementation YourViewController

- (void)loadProducts {
    // 定义产品标识符数组
    NSArray *productIdentifiers = @[@"com.example.product1", @"com.example.product2"];
    NSSet *set = [NSSet setWithArray:productIdentifiers];
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
    request.delegate = self;
    [request start];
}

@end

在上述代码中,首先定义了一个包含产品标识符的数组 productIdentifiers,这些标识符应与在 App Store Connect 中创建的内购项目的产品 ID 一致。然后使用该数组创建一个 NSSet,并以此初始化 SKProductsRequest 对象。设置请求的代理为当前视图控制器(需遵守 SKProductsRequestDelegate 协议),最后调用 start 方法启动请求。

3.2 处理产品请求响应

当产品请求完成后,会调用代理方法 productsRequest:didReceiveResponse:。在这个方法中,我们可以获取到请求的商品信息,并进行处理。

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    self.products = [NSMutableArray array];
    for (SKProduct *product in response.products) {
        [self.products addObject:product];
        NSLog(@"Product title: %@, price: %@, description: %@", product.localizedTitle, product.price, product.localizedDescription);
    }
    for (NSString *invalidProductId in response.invalidProductIdentifiers) {
        NSLog(@"Invalid product identifier: %@", invalidProductId);
    }
    // 在这里可以根据获取到的商品信息更新 UI,展示可购买的商品列表
}

productsRequest:didReceiveResponse: 方法中,首先清空之前可能存在的商品数组 self.products,然后遍历 response.products,将有效的商品添加到数组中,并打印商品的标题、价格和描述信息。同时,对于 response.invalidProductIdentifiers 中的无效产品标识符,也进行打印记录,以便排查问题。最后,可以根据获取到的商品信息更新应用的用户界面,展示可购买的商品列表给用户。

四、发起支付请求

当用户选择要购买的商品后,应用需要发起支付请求。

4.1 创建支付对象

根据获取到的商品信息,创建 SKPayment 对象。以下是创建支付对象的代码示例:

- (void)purchaseProduct:(SKProduct *)product {
    SKPayment *payment = [SKPayment paymentWithProduct:product];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

在上述代码中,purchaseProduct: 方法接收一个 SKProduct 对象作为参数,该对象代表用户要购买的商品。通过 [SKPayment paymentWithProduct:product] 创建支付对象 payment,然后使用 [[SKPaymentQueue defaultQueue] addPayment:payment] 将支付请求添加到支付队列中,发起支付流程。

4.2 处理支付队列代理方法

支付队列需要一个代理来处理支付过程中的各种事件,如支付成功、失败、取消等。在视图控制器中遵守 SKPaymentTransactionObserver 协议,并实现相关代理方法。

@interface YourViewController () <SKPaymentTransactionObserver>

@end

@implementation YourViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchased:
                [self completeTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                [self failedTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:
                [self restoreTransaction:transaction];
                break;
            default:
                break;
        }
    }
}

- (void)completeTransaction:(SKPaymentTransaction *)transaction {
    NSLog(@"Transaction completed successfully.");
    // 处理购买成功逻辑,如解锁功能、发送通知等
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

- (void)failedTransaction:(SKPaymentTransaction *)transaction {
    if (transaction.error.code != SKErrorPaymentCancelled) {
        NSLog(@"Transaction failed with error: %@", transaction.error.localizedDescription);
    } else {
        NSLog(@"Transaction cancelled by user.");
    }
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

- (void)restoreTransaction:(SKPaymentTransaction *)transaction {
    NSLog(@"Transaction restored successfully.");
    // 处理恢复购买逻辑,如恢复之前购买的内容
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

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

@end

viewDidLoad 方法中,将当前视图控制器添加为支付队列的事务观察者 [[SKPaymentQueue defaultQueue] addTransactionObserver:self]。在 paymentQueue:updatedTransactions: 方法中,遍历事务数组 transactions,根据事务状态 transactionState 进行不同的处理。

当事务状态为 SKPaymentTransactionStatePurchased 时,调用 completeTransaction: 方法,在该方法中可以处理购买成功的逻辑,如解锁应用内的功能、向用户发送购买成功的通知等,最后调用 [[SKPaymentQueue defaultQueue] finishTransaction:transaction] 结束该事务。

当事务状态为 SKPaymentTransactionStateFailed 时,调用 failedTransaction: 方法。如果错误码不是 SKErrorPaymentCancelled(即用户取消支付),则打印错误信息;若是用户取消支付,则打印相应提示。同样,最后调用 finishTransaction: 结束事务。

当事务状态为 SKPaymentTransactionStateRestored 时,调用 restoreTransaction: 方法,用于处理恢复购买的逻辑,如恢复用户之前购买过但因某些原因丢失的内容,最后也调用 finishTransaction: 结束事务。

在视图控制器销毁时,通过 [[SKPaymentQueue defaultQueue] removeTransactionObserver:self] 移除事务观察者,以避免内存泄漏和不必要的回调。

五、消耗型商品处理

消耗型商品是指用户购买后可以多次使用,使用后即消耗掉的商品,如游戏中的虚拟货币。

5.1 购买处理

在购买成功的回调方法 completeTransaction: 中,对于消耗型商品,除了完成事务,还需要更新应用内的相关数据,如增加用户的虚拟货币数量。

- (void)completeTransaction:(SKPaymentTransaction *)transaction {
    SKProduct *product = transaction.payment.product;
    if ([product.productIdentifier isEqualToString:@"com.example.consumableProduct"]) {
        // 假设应用中有一个管理虚拟货币的类,这里调用其方法增加虚拟货币数量
        [CurrencyManager sharedInstance].currencyCount += 100; // 假设购买一次增加 100 个虚拟货币
    }
    NSLog(@"Transaction completed successfully.");
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

在上述代码中,首先获取购买的商品 SKProduct *product = transaction.payment.product,然后通过判断商品标识符 product.productIdentifier 是否为消耗型商品的标识符,来确定是否为消耗型商品的购买。若是,则调用相关方法更新应用内的虚拟货币数量。

5.2 防止重复消耗

为了防止消耗型商品被重复消耗,通常需要在服务器端记录购买记录。当用户再次购买时,先查询服务器确认是否已经购买过该商品。以下是一个简单的示例,假设使用 HTTP 请求与服务器交互:

- (void)purchaseConsumableProduct {
    SKProduct *product = [self findProductWithIdentifier:@"com.example.consumableProduct"];
    if (product) {
        NSURL *url = [NSURL URLWithString:@"https://example.com/checkPurchase"];
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
        request.HTTPMethod = @"POST";
        NSString *parameters = [NSString stringWithFormat:@"productId=%@&userId=%@", product.productIdentifier, [UserManager sharedInstance].userId];
        request.HTTPBody = [parameters dataUsingEncoding:NSUTF8StringEncoding];
        NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            if (!error && data) {
                NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
                if ([[responseDict objectForKey:@"isPurchased"] boolValue]) {
                    // 已购买,直接处理购买成功逻辑
                    [self completeTransaction:nil];
                } else {
                    // 未购买,发起支付请求
                    SKPayment *payment = [SKPayment paymentWithProduct:product];
                    [[SKPaymentQueue defaultQueue] addPayment:payment];
                }
            }
        }];
        [task resume];
    }
}

在上述代码中,purchaseConsumableProduct 方法首先查找消耗型商品。然后构造一个 HTTP POST 请求,向服务器发送商品 ID 和用户 ID。服务器根据接收到的信息查询数据库,判断该用户是否已经购买过该商品,并返回相应的结果。如果服务器返回已购买,则直接调用 completeTransaction: 方法处理购买成功逻辑;若返回未购买,则发起支付请求。

六、非消耗型商品处理

非消耗型商品是指用户购买一次后,可永久使用的商品,如解锁应用的高级功能。

6.1 购买处理

在购买成功的回调方法 completeTransaction: 中,对于非消耗型商品,主要是解锁相关功能。

- (void)completeTransaction:(SKPaymentTransaction *)transaction {
    SKProduct *product = transaction.payment.product;
    if ([product.productIdentifier isEqualToString:@"com.example.nonConsumableProduct"]) {
        // 解锁高级功能,例如更改应用设置
        [SettingsManager sharedInstance].isAdvancedFeatureUnlocked = YES;
    }
    NSLog(@"Transaction completed successfully.");
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

在上述代码中,同样先获取购买的商品,通过判断商品标识符确定是否为非消耗型商品。若是,则将应用设置中的高级功能解锁标志 isAdvancedFeatureUnlocked 设置为 YES,以解锁高级功能。

6.2 恢复购买

非消耗型商品通常需要提供恢复购买功能,以便用户在更换设备或重新安装应用后,能够恢复之前购买的内容。在 restoreTransaction: 方法中处理恢复购买逻辑。

- (void)restoreTransaction:(SKPaymentTransaction *)transaction {
    SKProduct *product = transaction.payment.product;
    if ([product.productIdentifier isEqualToString:@"com.example.nonConsumableProduct"]) {
        // 恢复高级功能,例如更改应用设置
        [SettingsManager sharedInstance].isAdvancedFeatureUnlocked = YES;
    }
    NSLog(@"Transaction restored successfully.");
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

restoreTransaction: 方法中,判断恢复的商品是否为非消耗型商品,若是,则同样解锁高级功能,确保用户在恢复购买后能够继续使用之前购买的非消耗型商品对应的功能。

七、订阅型商品处理

订阅型商品是指用户需要定期付费以持续使用的商品,如按月或按年订阅的高级会员服务。

7.1 购买处理

在购买成功的回调方法 completeTransaction: 中,对于订阅型商品,除了完成事务,还需要处理订阅相关的逻辑,如记录订阅开始时间、设置订阅状态等。

- (void)completeTransaction:(SKPaymentTransaction *)transaction {
    SKProduct *product = transaction.payment.product;
    if ([product.productIdentifier isEqualToString:@"com.example.subscriptionProduct"]) {
        // 记录订阅开始时间
        [SubscriptionManager sharedInstance].subscriptionStartDate = [NSDate date];
        // 设置订阅状态为已订阅
        [SubscriptionManager sharedInstance].isSubscribed = YES;
    }
    NSLog(@"Transaction completed successfully.");
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

在上述代码中,通过判断商品标识符确定是否为订阅型商品。若是,则使用 SubscriptionManager 类记录订阅开始时间,并设置订阅状态为已订阅。

7.2 检查订阅状态

定期检查用户的订阅状态,以确保只有订阅用户能够使用相应的功能。可以在应用启动时或定期执行该检查。

- (void)checkSubscriptionStatus {
    NSDate *currentDate = [NSDate date];
    if ([SubscriptionManager sharedInstance].isSubscribed) {
        NSTimeInterval subscriptionDuration = [[SubscriptionManager sharedInstance].subscriptionStartDate timeIntervalSinceNow];
        // 假设订阅周期为一个月(以秒为单位)
        NSTimeInterval oneMonth = 30 * 24 * 60 * 60;
        if (subscriptionDuration < -oneMonth) {
            // 订阅已过期,更新订阅状态
            [SubscriptionManager sharedInstance].isSubscribed = NO;
        }
    }
    // 根据订阅状态更新 UI,例如显示或隐藏高级功能入口
    [self updateUIAccordingToSubscriptionStatus];
}

checkSubscriptionStatus 方法中,首先获取当前日期 currentDate。如果用户处于已订阅状态,计算从订阅开始时间到当前时间的时间间隔 subscriptionDuration。将该时间间隔与订阅周期(这里假设为一个月)进行比较,如果时间间隔超过订阅周期,则表示订阅已过期,更新订阅状态为未订阅。最后根据订阅状态更新应用的用户界面,如显示或隐藏高级功能入口。

八、错误处理

在整个内购流程中,可能会遇到各种错误,如网络问题、支付失败等。正确处理这些错误对于提供良好的用户体验至关重要。

8.1 支付失败错误处理

failedTransaction: 方法中,已经对支付失败的错误进行了初步处理。对于一些常见的错误,如网络连接问题、支付被拒等,可以给用户提供相应的友好提示。

- (void)failedTransaction:(SKPaymentTransaction *)transaction {
    if (transaction.error.code != SKErrorPaymentCancelled) {
        NSString *errorMessage = @"支付失败,请稍后重试。";
        if (transaction.error.code == SKErrorPaymentNotAllowed) {
            errorMessage = @"您的设备不允许进行此支付操作。";
        } else if (transaction.error.code == SKErrorUnknown) {
            errorMessage = @"发生未知错误,请联系支持人员。";
        }
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"支付失败" message:errorMessage preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
        [alertController addAction:okAction];
        [self presentViewController:alertController animated:YES completion:nil];
    } else {
        NSLog(@"Transaction cancelled by user.");
    }
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

在上述代码中,除了用户取消支付的情况,根据不同的错误码给用户提供更具体的错误提示。例如,当错误码为 SKErrorPaymentNotAllowed 时,提示用户设备不允许进行此支付操作;当错误码为 SKErrorUnknown 时,提示用户发生未知错误并联系支持人员。通过 UIAlertController 弹出提示框告知用户支付失败的原因。

8.2 产品请求错误处理

在产品请求过程中,如果发生错误,会调用 SKProductsRequestDelegaterequest:didFailWithError: 方法。在该方法中可以对错误进行处理。

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
    NSLog(@"Product request failed with error: %@", error.localizedDescription);
    NSString *errorMessage = @"获取商品信息失败,请检查网络连接。";
    if (error.code == SKErrorPaymentInvalidProductIdentifier) {
        errorMessage = @"无效的产品标识符,请联系开发者。";
    }
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"获取商品信息失败" message:errorMessage preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
    [alertController addAction:okAction];
    [self presentViewController:alertController animated:YES completion:nil];
}

request:didFailWithError: 方法中,首先打印错误信息。然后根据不同的错误码提供相应的错误提示。例如,当错误码为 SKErrorPaymentInvalidProductIdentifier 时,提示用户产品标识符无效并联系开发者。同样通过 UIAlertController 弹出提示框告知用户获取商品信息失败的原因。

九、测试内购

在正式发布应用前,需要对内购功能进行全面测试,以确保其稳定性和准确性。

9.1 使用沙盒环境

苹果提供了沙盒环境用于内购测试,开发者可以使用沙盒测试账号进行购买操作,而不会产生实际费用。在 Xcode 中,确保应用运行在沙盒环境下。进入 “Product” 菜单,选择 “Scheme”,点击 “Edit Scheme”。在 “Run” 选项的 “Arguments” 标签中,添加 “-AppleInternalTesting YES” 作为启动参数,这样应用就会连接到沙盒环境。

9.2 模拟各种场景

  1. 成功购买:使用沙盒测试账号进行购买操作,验证购买成功后应用内功能的解锁或数据更新是否正常。例如,对于消耗型商品,检查虚拟货币数量是否正确增加;对于非消耗型商品,确认高级功能是否成功解锁;对于订阅型商品,查看订阅状态和相关记录是否正确设置。
  2. 支付失败:模拟网络连接中断、支付被拒等支付失败场景,检查应用是否能正确处理错误,并给用户提供友好的提示。在网络连接中断场景下,可以通过关闭设备网络或在模拟器中设置网络限制来模拟。对于支付被拒场景,可以在 App Store Connect 中设置相关的测试条件。
  3. 恢复购买:在不同设备上使用同一沙盒测试账号进行购买,然后在其中一台设备上删除应用并重新安装,测试恢复购买功能是否正常。确保恢复购买后,之前购买的非消耗型商品和订阅型商品的功能能够正常使用。

通过全面测试各种内购场景,可以及时发现并解决潜在的问题,确保用户在正式使用应用内购功能时能够获得良好的体验。