锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

iOS 内购(In-App Purchase)详解

时间:2022-10-31 05:30:01 二极管模块mee75

iOS 内购(In-App Purchase)详解

概述

IAP 全称:In-App Purchase,是指苹果 App Store 苹果是在应用内购买的 App 购买虚拟商品或服务提供的交易系统。

适用范围:在 App 游戏道具、电子书、音乐、视频、订阅会员、App需要使用高级功能等 IAP,而在 App 购买实体商品(如淘宝购买手机)或不在 App 虚拟商品(如充电费)或服务(如滴滴叫车)不适用于 IAP。

简而言之,苹果规定必须使用适用范围内的虚拟商品或服务 IAP 购买支付不允许使用支付宝、微信支付等第三方支付方式(包括Apple Pay),不允许以任何方式跳出(包括跳出)App、提示文案等)引导用户通过应用外部渠道购买。

内购前准备

APP内集成IAP在开发帐户之前,需要先开发代码ITunes Connect以下三步:

1.后台填写银行账户信息;

2.配置产品信息,包括产品ID,产品价格等;

3.测试配置IAP沙箱账户支付功能。

产品管理人员一般负责填写银行账户信息,开发人员不需要注意,开发人员需要注意第二步和第三步。

填写银行账户信息

关于如何去 Itunes Connect 后台填写账户信息,本文不讨论,可参考:iOS内购一站式-填写账户信息

配置内购商品

IAP 它是一个商品交易系统,而不是一个简单的支付系统,每个购买项目都需要在开发者的背景下Itunes Connect后台为 App 创建相应的商品,提交苹果审批后,购买项目生效。内部购买商品有四种类型:

  • 消费项目:产品只能使用一次,使用后无效,必须再次购买,如:游戏币、一次性虚拟道具等;
  • 非消费项目:只需购买一次,不会过期或随使用而减少。如:电子书;
  • 自动续订:允许用户在固定时间内购买动态内容产品。此类订阅将自动续订,如:Apple Music这类按月订阅的商品;
  • 非续期订阅:允许用户购买有限服务的产品 App 内部购买项目的内容可以是静态的。此类订阅不会自动续期。

配置商品信息需要注意产品ID和产品价格

1,产品 ID 建议使用具有独特性的项目 Bundle Identidier 作为前缀后拼接自定义的唯一商品名称或 ID(字母、数字),这里有一个坑:一旦新建内购商品,其产品ID它将永远被占用。即使商品已被删除,除产品外,已创建的内购商品也将被占用 ID 所有其他信息都可以修改,如果删除内购商品,将无法创建相同的产品 ID 商品也意味着产品 ID 永久失效。

2,在创建IAP项目的时候,需要设定价格,产品价格只能从苹果提供的价格等级去选择,这个价格等级是固定的,同一价格等级会对应各个国家的货币,比如等级1对应1美元、6元人民币,等级2对应2美元、12元人民币……最高级87对应9999.99美元,6498元。此外,它可能是为了照顾一些货币区的开发者和用户,以及一些特殊的等级,如1美元、1元、1元、1美元、3元。除此之外,IAP项目不能定一个9.9元不符合任何等级。详见苹果官方价格等级文件。苹果的价格等级表通常不会调整,但也不排除在某些货币汇率发生巨大变化时,苹果会在调整前发送电子邮件通知开发者。

三、商品分成,App Store上的付费App和App苹果和开发商默认分为3/7。但事实上,苹果在某些地区与开发者分享之前需要扣除交易税,开发者的实际分享不一定是70%。自2015年10月以来,苹果一直在中国App Store购买扣除了中国账户购买的2%的交易税IAP,开发人员的实际分是68%~69%之间,中国以外不同地区的交易税标准也不同。

配置沙箱测试账号

在推出新的内部购买产品之前,测试人员通常需要测试内部购买产品,但内部购买涉及资金,所以苹果提供了内部购买测试 沙箱测试账号 的功能,Apple Pay 推出之后 沙箱测试账号`也可以用于 Apple Pay 支付测试,沙箱测试账号 简单理解是:只能用于内购和内购 Apple Pay 测试功能的 Apple ID,不是真的 Apple ID。

填写沙箱测试账号信息时应注意以下几点:

  • 电子邮件不能由其他人注册 AppleID 的邮箱;
  • 电子邮件不能是真正的邮箱,但必须符合邮箱格式;
  • App Store 区域选择、弹出提示框和结算价格将根据沙箱账户选择的区域进行测试。建议在测试过程中建立几个不同区域的新账户进行测试。

使用沙箱账号测试:

  • 首先,沙箱测试账户必须在真机环境下进行测试 adhoc 证书或者 develop 沙箱账号不支持证书签名的安装包 App Store 下载安装包;
  • 去真机的 App Store 退出真实的 Apple ID 账户退出后不需要App Store 登录沙箱测试账号;
  • 然后去 App 测试购买商品时,会弹出登录框,选择使用现有商品 Apple ID,然后登录沙箱测试账号,成功登录后弹出购买提示框,点击购买,然后弹出购买提示框。

内购流程

  • 获取内购产品列表(从(从)App内读或从自己的服务器读取),向用户展示内购列表
  • 用户选择内购产品后,首先要求可用内购产品的本地化信息列表,此次调用Apple的StoreKit库的代码
  • 获得内购产品的本地化信息后,根据用户选择的内购产品的ID获得内购产品
  • 根据内购产品发起IAP购买请求,收到购买完成的回调
  • 购买过程结束后, 请求向服务器发送验证凭证和支付结果
  • 服务器接收iOS购买凭证,判断凭证是否存在或验证,然后存储凭证。将凭证发送到苹果的服务器进行验证,并将验证结果返回给客户端
  • 您的服务器将支付结果信息返回到前端并发布虚拟产品

流程图如下:

在这里插入图片描述

代码逻辑:

---------------------LCLInAppPurchase.h--------------------- #import  #import   static NSString *InAppPurchaseFailRefuse = @"该商品暂时无法购买,请稍后重试"; static NSString *InAppPurchaseFailRequest = @"操作失败,请稍后重试"; static NSString *InAppPurchaseFailBuy = @"购买失败,请稍后重试"; static NSString *InAppPurchaseFailResume = @"恢复失败,你还没买过这个产品";    @interface LCLInAppPurchase : NSObject  - (id)init;  ///发起内购 - (void)launchInAppPurchase:(NSString *)productId;  //恢复内购 - (void)resumeInAppPurchase:(NSString *)productId ; -(void)removeObserver;   @end  
---------------------LCLInAppPurchase.m--------------------- #import "LCLInAppPurchase.h"   @interface LCLInAppPurchase() {     int _isResume;购买是否恢复     NSString *_productId;///内购产品ID } @end  @implementation LCLInAppPurchase   - (id)init{     self = [super init];               if (self) {         [[SKPaymentQueue defaultQueue] addTransactionObserver:self];              }          return self; }  -(void)removeObserver{     [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; } - (void)launchInAppPurchase:(NSString *)productId{         _isResume = 0;     _productId = productId;     if([SKPaymentQueue canMakePayment]){
        [self requestProductData:productId];
    }else{
        NSLog(@"不允许程序内付费");
    }
}
- (void)resumeInAppPurchase:(NSString *)productId{
    _isResume=1;
    _productId = productId;
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
- (void)requestProductData:(NSString *)type{
    NSLog(@"-------------请求对应的产品信息----------------");
    NSArray *product = [[NSArray alloc] initWithObjects:type, nil];
    NSSet *nsset = [NSSet setWithArray:product];
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
    request.delegate = self;
    [request start];
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    NSLog(@"--------------收到产品反馈消息---------------------");
    NSArray *product = response.products;
    if([product count] == 0){
        NSLog(@"--------------没有商品------------------");
        return;
    }
    NSLog(@"productID:%@", response.invalidProductIdentifiers);
    SKProduct *p = nil;
    for (SKProduct *pro in product) {
        NSLog(@"%@", [pro description]);
        NSLog(@"%@", [pro localizedTitle]);
        NSLog(@"%@", [pro localizedDescription]);
        NSLog(@"%@", [pro price]);
        NSLog(@"%@", [pro productIdentifier]);
        if([pro.productIdentifier isEqualToString:_productId]){
            p = pro;
        }
    }
    SKPayment *payment = [SKPayment paymentWithProduct:p];
    NSLog(@"发送购买请求");
    [[SKPaymentQueue defaultQueue] addPayment:payment];
    // 可以把我们的自己订单和IAP的交易订单绑定,本地存储订单信息
}
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
    NSLog(@"------------------错误-----------------:%@", error);
}
- (void)requestDidFinish:(SKRequest *)request{
    NSLog(@"------------反馈信息结束-----------------");
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
    NSString *resultA=@"";
    SKPaymentTransaction *tran = [transaction lastObject];
    switch (tran.transactionState) {
        case SKPaymentTransactionStatePurchased:
            NSLog(@"交易完成");
            if (_isResume==0) {
                NSData *receiptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
                resultA=[self encode:receiptData.bytes length:receiptData.length];
                NSLog(@"购买结果票据:%@",resultA);
                // 收据发送到服务器
                // 收据验证成功之后结束交易
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                 // 删除保存的订单信息
            }
            else
            {
                NSString *resultB=[self encode:tran.transactionReceipt.bytes length:tran.transactionReceipt.length];
                NSLog(@"恢复结果票据:%@",resultB);
                // 收据发送到服务器
                // 收据验证成功之后结束交易
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
            }
            
            break;
        case SKPaymentTransactionStatePurchasing:
            NSLog(@"商品添加进列表");
            break;
        case SKPaymentTransactionStateRestored:
            NSLog(@"已经购买过商品");
            [[SKPaymentQueue defaultQueue] finishTransaction:tran];
            break;
        case SKPaymentTransactionStateFailed:
            NSLog(@"交易失败");
            NSLog(@"%ld",tran.error.code);
            [[SKPaymentQueue defaultQueue] finishTransaction:tran];
            [self errorReason:tran.error];
            break;
        default:
            break;
    }
}
- (void)errorReason:(NSError *)error{
    NSString *detail;
    if (error != nil) {
        switch (error.code) {
            case SKErrorUnknown:
                NSLog(@"SKErrorUnknown");
                detail = @"未知的错误,您可能正在使用越狱手机";
                break;
            case SKErrorClientInvalid:
                NSLog(@"SKErrorClientInvalid");
                detail = @"当前苹果账户无法购买商品(如有疑问,可以询问苹果客服)";
                break;
            case SKErrorPaymentCancelled:
                NSLog(@"SKErrorPaymentCancelled");
                detail = @"订单已取消";
                break;
            case SKErrorPaymentInvalid:
                NSLog(@"SKErrorPaymentInvalid");
                detail = @"订单无效(如有疑问,可以询问苹果客服)";
                break;
            case SKErrorPaymentNotAllowed:
                NSLog(@"SKErrorPaymentNotAllowed");
                detail = @"当前苹果设备无法购买商品(如有疑问,可以询问苹果客服)";
                break;
            case SKErrorStoreProductNotAvailable:
                NSLog(@"SKErrorStoreProductNotAvailable");
                detail = @"当前商品不可用";
                break;
            default:
                NSLog(@"No Match Found for error");
                detail = @"未知错误";
                break;
        }
    }
}
- (NSString *)encode:(const uint8_t *)input length:(NSInteger)length {
    static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    NSMutableData *data = [NSMutableData dataWithLength:((length + 2) / 3) * 4];
    uint8_t *output = (uint8_t *)data.mutableBytes;
    for (NSInteger i = 0; i < length; i += 3) {
        NSInteger value = 0;
        for (NSInteger j = i; j < (i + 3); j++) {
            value <<= 8;
            if (j < length) {
                value |= (0xFF & input[j]);
            }
        }
        NSInteger index = (i / 3) * 4;
        output[index + 0] =                    table[(value >> 18) & 0x3F];
        output[index + 1] =                    table[(value >> 12) & 0x3F];
        output[index + 2] = (i + 1) < length ? table[(value >> 6)  & 0x3F] : '=';
        output[index + 3] = (i + 2) < length ? table[(value >> 0)  & 0x3F] : '=';
    }
    return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
}
- (void)dealloc{
    [self removeObserver];
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

@end

自动续期订阅

自动续期订阅需要增加一个参数password,秘钥在APP内购买项目处创建。服务器提供URL用以接收苹果服务器通知,包含订阅状态变更或App内购买项目退款等。

丢单及其他问题处理

IAP的支付流程:

1,发起支付

2,扣费成功

3,得到receipt(支付凭据)

4,去后台验证凭据获取商品交易状态

5,返回数据,验证成功前端刷新数据

  • 漏单情况一:2到3环节出问题属于苹果的问题,目前没做处理。

  • 漏单情况二:3到4的时候出问题,比如断网。此时前端会把支付凭据持久化存储下来,如果期间用户卸载APP此单在前端就真漏了,如果没有协助,下次重新打开app进入购买页会先判断有无未成功的支付,有就提示用户,用户选择找回,重走4,5流程。这一步看产品需求怎么做,可以让用户自主选择是否恢复未成功的支付也可以前端默默恢复就行。

  • 漏单情况三:4到5的时候出问题。此时后台其实已经成功,只是前端没获取到数据,当漏单处理,下次进入的时候先刷新数据即可。

  • 交易凭据receipt判重。一般来说验证支付凭据(receipt)是否有效放后台去做,如果后台不做判重,同一个凭据就可以无数次验证通过,因为苹果也不判重,这就会导致前端可以凭此取到的一个支付凭据可以去后台无数次做校验

锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章