iOS应用内购详解

一、内购支购买项目类型:

类型说明:

  • 1.消耗型商品 :只可使用一次的产品,使用之后即失效,必须再次购买。
    示例:抖音的打赏。

  • 2.非消耗型商品:只需购买一次,不会过期或随着使用而减少的产品。
    示例:游戏中的 角色。

  • 3.非续期订阅:允许用户购买有时限性服务的产品。此 App 内购买项目的内容可以是静态的。此类订阅不会自动续期。
    示例:为期一年的已归档文章目录订阅。

  • 4.自动续期订阅:允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期。
    示例:每月订阅提供流媒体服务的 App,腾讯视频自动续订月会员。

类型注意事项:

自动续期订阅类型最麻烦,是有连续性的,其中还有免费试用期、促销期的概念,用户还可以取消续订,恢复续订等。

收益相关:

1、2、3三种类型,您的收益率为70%。

自动续期订阅类型:
我的订阅设置如何影响收益率?
在订阅者使用付费服务的首年内,您的收益率为 70%。当订阅者为同一订阅群组中的订阅产品累积一年的付费服务后,您的收益率将提高至 85%。同一群组中的升级订阅、降级订阅和跨级订阅不会中断付费服务的天数。转换至不同群组的订阅将重置付费服务的天数。赚取 85% 订阅价格这一规则适用于2016年6月之后生效的订阅续期。
当订阅被取消时,我的收益率将如何变化?
当订阅被取消时,付费服务天数将停止累积,并开始为期 60 天的宽限期。如果订阅由于账单问题而未续期,Apple 将尝试续期该订阅,且该订阅将处于“Billing Retry(计费重试)”状态。在此期间,付费服务天数不计入其中。如果用户在 60 天的宽限期内重新订阅或在宽限期内的计费重试期间恢复订阅,付费服务天数将继续累积至 85% 的收益率。如果用户在 60 天宽限期结束后重新订阅,付费服务天数将被重置,且您的收益率为 70%。每一次订阅过期,都会开始一个新的 60 天宽限期。

苹果官方网址:

https://help.apple.com/app-store-connect/#/dev3cd978dbd
https://developer.apple.com/in-app-purchase/


二、内购支付流程:

1.客户端根据产品的productIdentifiers,向Appstore请求购买产品的详细信息,确保产品信息在苹果服务器是否存在;

2.APP验证产品成功后,发送购买请求,添加支付状态的监听;

3.先相应商品添加进列表(SKPaymentTransactionStatePurchasing)方法,然后是交易完成(SKPaymentTransactionStatePurchased),Appstore向客户端返回一段receipt-data,里面记录了本次交易的证书和签名信息。

4.客户端 或 服务器 把编码后的receipt-data发往itunes.appstore进行验证(区分沙盒环境、正式环境)

//沙盒测试环境验证
#define SANDBOX     @"https://sandbox.itunes.apple.com/verifyReceipt"
//正式环境验证
#define AppStore    @"https://buy.itunes.apple.com/verifyReceipt"

5.服务器验证凭证是否合法,对用户业务操作(成功增加一个月会员),并返回客户端进行后续业务逻辑处理刷新UI;


三、itunes connect申请内购

3.1、协议证书相关:

1.进itunes connnet最外层开发者平台,点击协议:

2.查看付费同意条款:

3.设置付费协议,添加银行卡账号,税务表和联系信息



4.填写完成后,等待苹果审核一段时间,这里变成有效后,才能进行沙盒测试,否则找不到产品信息

3.2、添加内购项目:

1.在你上线的APP中,APP-功能-APP内购买项目

2.选择添加的类型

3.设置产品价格和名称

4.设置显示信息

5.审核信息,可以先不填,后期测试时截图补全

3.3、创建沙盒技术测试号:

APP store Connect -> 用户和访问 -> 沙箱技术 -> 测试员

不能是已经使用的APPLE ID账号,容易后期混乱。
可以新申请几个QQ,然后激活邮箱;
然后填写 新测试员信息,密码必有大小写字母;
邮箱收到邮件,激活后添加沙盒测试账号成功。


四、具体代码和实现:

#import <StoreKit/StoreKit.h>


//沙盒测试环境验证
#define SANDBOX     @"https://sandbox.itunes.apple.com/verifyReceipt"
//正式环境验证
#define AppStore    @"https://buy.itunes.apple.com/verifyReceipt"


@interface SubmitOrderVC ()<UITableViewDelegate,UITableViewDataSource,SKPaymentTransactionObserver,SKProductsRequestDelegate>

@property (nonatomic, strong) BaseTableView *centerTableView;

@end

static NSString *IdentifySubmitOrderCell         = @"SubmitOrderCell";
static NSString *IdentifySubmitOrderAmountCell   = @"SubmitOrderAmountCell";
#define KTitleListArray  @[@"商品",@"会员服务时间",@"优惠说明",@"初始价格",@"新用户红包优惠"]


@implementation SubmitOrderVC

//沙盒测试环境验证
#define SANDBOX     @"https://sandbox.itunes.apple.com/verifyReceipt"
//正式环境验证
#define AppStore    @"https://buy.itunes.apple.com/verifyReceipt"

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self checkOrderStatus];
}
/** 检测客户端与服务器漏单情况处理*/
- (void)checkOrderStatus
{
    NSDictionary *orderInfo = [self getReceiptData];
    if (orderInfo != nil) {
        
        NSString *orderId = orderInfo[@"userID"];
        NSString *receipt = orderInfo[@"receipt"];
        
        if ([[UserModel currentUser].display_name isEqualToString:orderId]) {
            [self appVerificationReceipt:receipt];
        }
    }
}



#pragma mark - 内购
- (void)intoChooseBuyWayVC:(UIButton *)btn {
    ///    [ANCustomHUD showLoadingText:@"正在拉起苹果内购" View:nil];
    if ([SKPaymentQueue canMakePayments]) {
        // 如果允许应用内付费购买
        [ANCustomHUD showLoadingText:@"正在拉起苹果内购" View:nil];

        // 把商品ID信息放入一个集合中
        NSSet * set = [NSSet setWithArray:@[@"caibaoshuo_lv1"]];
        // 请求内购商品信息,只返回你请求的产品(主要用于验证商品的有效性)
        SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
        request.delegate = self;
        [request start];
    } else {
        // 如果用户手机禁止应用内付费购买.
        // 则弹出开启购买权限开关的提示等...
    }
}
//获取请求完成和失败的结果
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
    NSLog(@"------------------错误-----------------:%@", error);
}
- (void)requestDidFinish:(SKRequest *)request{
    NSLog(@"------------反馈信息结束-----------------");
}
/**
 获取商品的查询结果
 SKProductsRequest是苹果封装好的一个对象,该对象有两个属性。
 products是一个数组,代表的是你获取到的所有商品信息,每个商品  都是一个数组元素。
 invalidProductIdentifiers是无效的商品id的数组,此id对应的是你在苹果后台构建的商品id。
 */
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response NS_AVAILABLE(10_7, 3_0); {
    
    [ANCustomHUD hiddenLoadingView:nil];
    NSLog(@"--------------收到产品反馈消息---------------------");
    NSArray *product = response.products;
    if([product count] == 0){
        NSLog(@"--------------没有商品------------------");
        return;
    }
    NSLog(@"productID:%@", response.invalidProductIdentifiers);
    NSLog(@"产品付费数量:%lu",(unsigned long)[product count]);
    
    SKProduct *p = product.lastObject;
    //    for (SKProduct *pro in product) {
    //        NSLog(@"%@", [pro description]);
    //        NSLog(@"%@", [pro localizedTitle]);
    //        NSLog(@"%@", [pro localizedDescription]);
    //        NSLog(@"%@", [pro price]);
    //        NSLog(@"%@", [pro productIdentifier]);
    //        p = pro;
    //        // 如果后台消费条目的ID与我这里需要请求的一样(用于确保订单的正确性)
    //        if([pro.productIdentifier isEqualToString:_currentProId]){
    //            p = pro;
    //        }
    //    }
    
    ///发送购买请求,创建支付
    SKPayment *payment = [SKPayment paymentWithProduct:p];
    //提交付款申请
    [[SKPaymentQueue defaultQueue] addPayment:payment];
    //支付运行时,一定要添加监听
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}

//监听购买结果,交易状态发生改变时,包括状态的改变,交易的结束 SKPaymentTransactionObserver,SKPaymentTransactionObserver 是交易观察者,
//https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/DeliverProduct.html
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions NS_AVAILABLE(10_7, 3_0);
{
    for(SKPaymentTransaction *tran in transactions){
        switch (tran.transactionState) {
            case SKPaymentTransactionStatePurchased:
            {  ///交易完成。事务在队列中,用户已被收费。更新UI以反映正在进行的状态,并等待再次调用。
                NSLog(@"交易完成");
                [self verifyPurchaseWithPaymentTransaction:tran ];
                
                
                [self finishTransactionTask:tran];
            }
                break;
            case SKPaymentTransactionStatePurchasing:
            {   //商品添加进列表,事物被添加到服务器队列中。提供购买的功能
                NSLog(@"商品添加进列表");
            }
                break;
            case SKPaymentTransactionStateRestored:
            {//事务从用户的购买历史记录中恢复。恢复以前购买的功能。
                NSLog(@"已经购买过商品");
                [self finishTransactionTask:tran];
            }
                break;
            case SKPaymentTransactionStateFailed:
            { //交易失败,购买失败或者用户取消。使用error属性的值向用户显示消息。
                [ANCustomHUD showError:@"购买失败" toView:nil];
                [self finishTransactionTask:tran];
            }
                break;
            case SKPaymentTransactionStateDeferred: {//未知状态。更新UI以反映延迟状态,并等待再次调用。
                [ANCustomHUD showError:@"最终状态未确定" toView:nil];
                [self finishTransactionTask:tran];
            }
                break;
            default:
                break;
        }
    }
}
/**
 *  验证购买,避免越狱软件模拟苹果请求达到非法购买问题
 票据的校验是保证内购安全完成的非常关键的一步,一般有三种方式:
 1、服务器验证,获取票据信息后上传至信任的服务器,由服务器完成与App Store的验证(提倡使用此方法,比较安全)
 2、本地票据校验
 3、本地App Store请求验证
 *
 */
-(void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction{
    ///解除监听
    //[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
    
    //从沙盒中获取交易凭证并且拼接成请求体数据
    NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];
    if (!receiptData) {
        return  ;
    }
    NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串
    NSLog(@"内购完成的收据为:%@",receiptString);
    [self saveReceiptData:@{@"receipt":receiptString,
                            @"userID":[UserModel currentUser].display_name}];
    [self appVerificationReceipt:receiptString];
    

}
///APP直接去苹果服务器验证
- (void)appVerificationReceipt:(NSString *)receiptString {
    NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接请求数据
    NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
    
    NSURL *url = [NSURL URLWithString:SANDBOX];
    NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:url];
    requestM.HTTPBody = bodyData;
    requestM.HTTPMethod = @"POST";
    
    // 创建连接并发送同步请求
    NSError *error = nil;
    NSData *responseData = [NSURLConnection sendSynchronousRequest:requestM returningResponse:nil error:&error];
    if (error) {
        NSLog(@"验证购买过程中发生错误,错误信息:%@",error.localizedDescription);
        return;
    }
    
    NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];
    NSLog(@"%@",dic);
    if ([dic[@"status"] intValue]==0) {
        NSLog(@"购买成功!");

        ///解除监听
        [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
        [self removeLocReceiptData];
    } else {
        
    }
}
//交易结束
- (void)completeTransaction:(SKPaymentTransaction *)transaction {
    NSLog(@"交易结束");
    [self finishTransactionTask:transaction];
}
///完成交易:完成交易前需要完成:坚持购买 或  下载相关内容  或 更新应用的UI以允许用户访问产品
- (void)finishTransactionTask:(SKPaymentTransaction *)transaction   {
    ///从队列中删除已完成(即失败或已完成)的事务。试图完成购买事务将引发异常。
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void)dealloc{
    ///解除监听
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

#pragma mark -- 本地保存一次支付凭证
static NSString *const kSaveReceiptData = @"kSaveReceiptData";

- (void)saveReceiptData:(NSDictionary *)receiptData
{
    [[NSUserDefaults standardUserDefaults] setValue:receiptData forKey:kSaveReceiptData];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

- (NSDictionary *)getReceiptData
{
    return [[NSUserDefaults standardUserDefaults] valueForKey:kSaveReceiptData];
}

- (void)removeLocReceiptData
{
    [[NSUserDefaults standardUserDefaults] removeObjectForKey:kSaveReceiptData];
    [[NSUserDefaults standardUserDefaults] synchronize];
}


五、注意事项:

申请相关:

需要注意的是如果应用是第一次进行IAP开发, 首先要完善苹果商店内的个人信息 (银行卡信息、 税务相关信息)才能创建相关商品, 而且需要在下一个发布版本中审核商品, 如果曾经审核过IAP开发, 可直接在后台进行新增商品审核。

测试相关:

1.内购必须用真机测试;
2.测试时必须退出App Store自己的Apple ID,登录沙盒的测试Apple ID.
3.本身请求美国服务器就慢,为防止审核人员误解,我们需要在购买时加载动画;
4.自动续期测试:https://help.apple.com/app-store-connect/#/dev7e89e149d

不允许强制用户必须登录才能购买:

因为苹果规定所有内购绑定的账号都应该是apple账号,所以不登陆你app自己的账号也应该可以购买,也就是游客状态下也要能购买,不然就耽误苹果赚钱了。
关于这个问题有两个解决办法:
(1)做游客模式可购买(未登录是绑定设备,下一个账号登录以后绑定账号)
(2)必须登录才可以使用app。
(3)绑定至当前设备,跟着设备走。例如腾讯会员

当然也可以做一个审核接口来应对。

自动续订订阅的说明一定要有:

自动续订订阅,一定要在app中有详细的说明。
除了在app里要写,在iTunes Connect的应用描述里也要写,以喜马拉雅为例,

丢单处理

由于IAP服务器无法保证质量, 或者自己服务器验证凭证出现问题时, 可能会出现丢单(用户付费成功, 但是凭证无法成功向自己服务器验证)的情况, 对于这种情况, 我们可以这样处理。

用户成功下单后,储存订单&uid&凭证。

存储 订单&uid&凭证
@param orderID 订单
@param uid 用户uid
@param receipt 凭证
@param saveKey 储存key
*/
- (void)saveOrderReceiptWithOrderID:(long long)orderID
                            uid:(NSString *)uid
                        receipt:(NSString *)receipt
                        saveKey:(NSString *)saveKey;

在用户向服务器验证成功后或者非网络原因造成的失败后, 删除此条记录,

删除 订单&凭证
@param orderID 订单
@param receipt 凭证
@param saveKey 储存key
*/
- (void)removeOrderReceiptWithOrderID:(long long)orderID
                          receipt:(NSString *)receipt
                          saveKey:(NSString *)saveKey;

这样如果由于网络问题或者服务器出现问题造成丢单, 我们可以在下一次用户启动APP再次去进行验证这笔订单, 重复上面流程

 核对支付成功但是验证失败的订单
*/
- (void)checkLocalLostVipOrder;
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值