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

  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Uni-app是一种跨平台的开发框架,可以方便地开发出适用于多个终端的应用程序,包括Web、iOS、Android等。在开发过程中,我们可以使用uni-app提供的API实现多种功能,比如调用微信支付。 首先,我们需要在应用程序内引入微信支付所需的SDK,在Uni-app中可以使用插件机制,自己开发一个插件或者使用已有的插件,如uni-wxpay等。然后,在APP内调用微信支付的流程如下: 1.在创建支付订单时,需要将订单信息传递给服务端,由服务端生成订单号、调用微信支付API生成预支付订单,并返回给APP。 2.APP拿到预支付订单后,调用微信SDK内置的API进行支付,主要包括支付参数的配置和支付的发起。 3.支付完成后,微信会回调我们在服务端注册的回调地址,服务端通过请求微信的API对支付结果进行核对,确认支付是否成功,并作出相应的处理。 需要注意的是,在调用微信支付API时需要在微信开放平台申请开发者账号,并完成相应的配置,包括设置支付回调地址、支付授权目录等等。 总之,通过调用微信支付API,我们可以为APP添加支付功能,实现线上商品购买、捐赠赞赏等功能。而在Uni-app中,使用插件机制可以更加方便快捷地完成这个流程。 ### 回答2: uni-app 是一个可以跨平台开发的框架,它支持开发微信小程序、支付宝小程序、H5 等多个平台。在 uni-app 中内调用微信支付可以实现用户在应用内进行支付,下面我将具体介绍 uni-app 中如何进行内调用微信支付。 1. 首先需要在应用中安装微信支付插件,打开 HBuilderX,选择菜单栏中的“插件市场”,搜索“微信支付”,选择安装。 2. 在应用中使用微信支付的页面中引入微信支付插件: ```javascript import $payment from "@/uni_modules/yk-payment/js_sdk/uni-payment.js"; // 引入插件 ``` 3. 在需要支付的位置,调用微信支付的方法: ```javascript uni.showLoading({ title: '加载中' }); $payment.weixinPay({ timeStamp: '1603388794', nonceStr: '5pnskrq5060pt2lljndzpta9hzqmxrsq', package: 'prepay_id=wx30163954528026d7bf482abf2becd37124', signType: 'MD5', paySign: '3ACA84580DD8C32D8478B4BBF3688A1D', success: function (res) { console.log('success:' + JSON.stringify(res)); }, fail: function (err) { console.log('fail:' + JSON.stringify(err)); }, complete: function (res) { uni.hideLoading(); } }); ``` 其中,微信支付需要提供以下参数: - timeStamp:时间戳,单位为秒 - nonceStr:随机字符串,不长于 32 位 - package:统一下单接口返回的 prepay_id,参考[微信支付开发文档](https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1)中获取订单号 - signType:签名算法,目前支持 MD5 和 HMAC-SHA256 - paySign:签名,具体签名方式详见[微信支付开发文档](https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1) 微信支付成功后会回调 success 回调函数,失败则回调 fail 回调函数。 4. 在微信支付的后端接口中,需要根据微信支付返回的结果进行签名校验,确保订单的真实性。 综上所述,使用 uni-app 内调用微信支付步骤相对简单,只需安装微信支付插件,调用支付方法即可,但支付过程中需要注意时间戳、随机字符串、签名等参数的正确性,同时在后端接口中校验微信支付的签名以确保支付的真实性。 ### 回答3: Uni-app是一个跨平台开发框架,它可以让开发者一次编写代码,同时在多个平台上运行。微信支付是一个广为人知的移动支付平台,它可以提供便捷的支付服务。在实践中,我们可以使用Uni-app中的支付插件来在应用程序中调用微信支付。下面是如何实现Uni-app应用程序内调用微信支付: 1. 首先,安装支付插件在Uni-app开发环境中,可以通过npm安装。 2. 在支付插件内部,我们需要引用微信支付的API,以便在我们的应用程序中调用这些API来实现支付。这些API包括支付API、查询订单API、退款API等等。 3. 接下来,我们需要在我们的代码中调用支付API,这个API用于请求加载微信支付。当用户点击订单支付按钮时,我们可以在后台发送一个请求,请求加载微信支付页面和所需的支付参数。 4. 在向微信支付发送请求后,我们需要接收来自微信支付的响应,然后将结果传递给我们的应用程序。通常,微信支付会将支付结果返回给我们的后台服务器,然后我们可以将结果传递给我们的应用程序。我们可以使用Uni-app的API来轮询服务器以获取结果。 5. 最后,我们需要在我们的应用程序中向用户显示支付结果。如果支付成功,我们可以向用户显示订单确认信息。如果支付失败,我们可以向用户显示错误信息。 总之,Uni-app应用程序的开发者可以使用插件实现在应用程序中调用微信支付。开发者需要在插件内部引用微信支付API,然后在代码中调用它们来实现支付。最后,我们需要接收来自微信支付的响应,并在我们的应用程序中向用户显示支付结果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mee_L

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值