iOS 内购In-App Purchase详解

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对应999.99美元、6498元人民币。另外可能是为了照顾某些货币区的开发者和用户,还有一些特殊的等级,比如备用等级A对应1美元、1元人民币,备用等级B对应1美元、3元人民币这样。除此之外,IAP项目不能定一个9.9元人民币这样不符合任何等级的价格。详细价格等级表可以看苹果的官方价格等级文档。苹果的价格等级表通常是不会调整的,但也不排除在某些货币汇率发生巨大变化的情况下,对该货币的定价进行调整,调整前苹果会发邮件通知开发者。

3,商品分成,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 <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>

static NSString *InAppPurchaseFailRefuse = @"该商品暂时无法购买,请稍后重试";
static NSString *InAppPurchaseFailRequest = @"操作失败,请稍后重试";
static NSString *InAppPurchaseFailBuy = @"购买失败,请稍后重试";
static NSString *InAppPurchaseFailResume = @"恢复失败,您未购买过该商品";



@interface LCLInAppPurchase : NSObject<SKPaymentTransactionObserver,SKProductsRequestDelegate>

- (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 canMakePayments]){
        [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)是否有效放后台去做,如果后台不做判重,同一个凭据就可以无数次验证通过,因为苹果也不判重,这就会导致前端可以凭此取到的一个支付凭据可以去后台无数次做校验

原文地址:https://blog.csdn.net/lichuanliangios

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


在有效期内的苹果开发者账号(类型为个人或者公司账号)。还有一种情况,就是你的Apple ID被添加到公司开发者账号团队里面,这样也是可以的,但是需要叫管理员给你开通相应的账号权限,如下截图:这里可能有些同学会问,苹果开发者账号是什么?如何申请?那么可以看看我的上一篇文章:iOS苹果开发者账号(公司账号)申请流程详解能正常编译打包的iOS工程项目(都不能正常编译谈何出包上架
Appuploader官网--IOS ipa上传发布工具,证书制作工具跨平台版,windows,linux,mac系统都可用 (applicationloader.net)第一步:注册苹果开发者账号,访问以下网址,按照提示注册即可,因为不需要支付688认证苹果开发者,所以没什么好讲的。证书部分:主要是通过工具生成.p12证书文件,后面这个证书要导入mac系统。描述文件:这个文件主要包含了证书,公钥,设备信息等,具体可以百度了解详情。第三步:使用xcode打包导出ipa文件,供其他人内测。..........
苹果在9月13号凌晨(北京时间)发布 iOS 16,该系统的设备可能会因为各种原因,导致功能不可用和UI错乱等问题,我们需要做好适配iOS 16。
计算机图形学--OpenGL递归实现光线追踪
Xcode 14打出来的包在低版本系统运行时会崩溃,报错信息是Library not loaded: /usr/lib/swift/libswiftCoreGraphics.dylib,在苹果开发者论坛搜索报错信息,可以看到会闪退的最高版本是iOS12.1(不敢肯定,毕竟我没测过,不过肯定低于iOS 12.4
iOS16手机开启开发者模式 "developer mode disable"Pod工程中的Bundle target签名报错
【计算机图形学】【实验报告】DDA画线算法、Bresenham中点画线算法、多边形填充算法(附代码)
iOS 16 满载全新的个性化功能、更具深度的智能技术,以及更多无缝的沟通与共享方式,让 iPhone 的体验更进一步。13、隐私权限增强,如通过 UIDevice 获取设备名称时,无法获取用户的信息,只能获取设备对应的名称。
3、回到苹果开发者中心,如下图,点击certificates,点蓝色小加号,就可以开始创建证书,创建证书的时候无论测试还是上传app store打包,都要选ios distribution app store and adhoc,不要选apple和develpment类型的证书。2、如下图,点左边的profiles菜单,点击蓝色加号,创建描述文件,创建过程中会要求我们选择描述文件的类型,假如你想发布app到app store,则选择app store,假如你想真机测试,则选择ad hoc类型。
需要:Unity,IOS平台模块,Xcode,IOS_SDk,MAC电脑,Iphone手机
最近下载安装 xcode 并解压安装,遇到一些问题误以为是错误,记录在此。从百度和谷歌上搜了很多帖子,发现并没有靠谱的 xcode 国内的镜像,这里提供一个可以跳转到官网的下载方式。xcode 不同版本的列表,下载时注意不同 macOs 不同的系统和 Xcode 版本的对应关系。如 要求 及其以上版本。https://xcodereleases.com/注意点击后会跳到官方下载地址,需要登录苹果账号。xcode 文件很大,通常要 10G 以上,下载速度很慢。可以考虑使用 Free Downlo
使用苹果登录作为第三方登录
missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun解决方法
Xcode14 正式版编译报错' does not contain bitcode.You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. file 'xxx' for architecture arm64解决方案
那应该和升级Xcode 14有关系。但是官方还没有给出解决方案。应该后续会有兼容的cocoapods 新版本。
项目中需要用到Xcode将C++代码输出的Mac版本的DLL文件即DYLIB文件,并能够使用C#代码调用。Unity与 DLL文件 ☀️| 怎样使用VC++生成一个DLL文件并调用!这次来看一下在Mac使用Xcode生成的方法吧!本文介绍了在Mac中怎样使用 Xcode项目使用C++生成 .dylib文件的方法。相对于在Windows中使用AndroidStudio创建动态链接库的方法来说,使用Xcode创建.dylib文件确实简单了很多。