iOS 应用内支付

参考链接
iOS开发之内购-AppStore
iOS App提交指南(二)-协议、税务和银行业务
iOS应用内付费(IAP)开发步骤列表

一、创建App

首先你需要登录 App的ItunesConnection,你会看到如下界面


ItunesConnection

简单的介绍一下这几个选项

  1. 我的App主要用于管理自己的App应用,例如编辑资料,上架,下架等。
  2. 销售和趋势主要是来查看App在各个平台的下载量,收入等方面数据,里面有曲线图等图文结合的方式给我们参考。
  3. 付款和财务报告显示的是你的收入以及付款等相关信息。
  4. iAd主要是跟广告有关,开发者可以登录到Workbench,通过iAd对应用的广告进行控制。
  5. 用户和职能用于生成相应账号,例如苹果沙河测试账号。
  6. 协议,税务和银行业务则是你银行相关账户的信息设置。

在这里我们选择第一个选项,我的App,然后点击左上角的加号,新建一个用来测试用的App。

新建App

点新建 App,会出现新建窗口;


新建窗口

在这里有几个需要填写的地方,名称自己取,平台IOS,语言选择了简体中文,套装ID也就是你的Bundle Identifier,需要你在Certificates页面 申请BundleID,SKU可以理解为用户看一看到的唯一标示,会体现在你的app的App Store的链接中。

申请BundleID

打开Certificates页面,在左侧选择Identifiers,并点击加号,申请一个新的Identifiers。


申请一个新的Identifiers

在这里Name可以随意填写,我填写的是TestAppStroeTestDemo,而用来使用的BundleID,我们在这里必须选择第一个选项唯一的,不用选择通配。在下面的选项中, 我们只需要勾选一个 Apple Pay即可,其他选项看自己需求,我在这里只选择了它。


选项

之后我们回到创建App,选择好自己刚创建的 BundleID ,填写SKU, SKU是你App的专用ID,我在这里随意填写,直接复制了App名。点击创建,我们的测试App则创建成功。

二、协议、税务和银行业务

进入协议、税务和银行业务页面


协议、税务和银行业务页面

进入协议、税务和银行业务页面后,会有3种合同类型,如果你之前没有主动申请过去合同,那么一般你现在激活的合同只有iOS Free Application一种。

页面内容分为两块:

  • Request Contracts(申请合同)
  • Contracts In Effect(已生效合同)。

合同类型分为3种:

  • iOS Free Application(免费应用合同)
  • iOS Paid Application(付费应用合同)
  • iAd App NetNetwork(广告合同)

笔者暂时只申请过付费应用合同,所以下面主要讲一下付费应用合同的申请流程。


付费应用合同

当我们点击申请iOS Paid Application合同后,该合同的状态会变成如下的样子,我们可以看到其中Status为Pending Tax, Bank, Contact。意思是联系方式、银行和税务信息没有填写。


缺少信息

1、填写联系方式

我们点击Contact Info下方的Set Up按钮可以进入联系方式填写页面,如下图:


填写联系方式

如果你没有添加过联系人,你需要通过Add New Contact按钮来添加一个新的联系人。然后指定联系人的职务,职务如下:

  • Senior Management:高管
  • Financial:财务
  • Technical:技术支持
  • Legal:法务
  • Marketing:市场推广

如果你是独立开发者,可以全部填你自己一个人。

2、填写银行卡信息

我们点击Bank Info下方的Set Up按钮可以进入联系方式填写页面,如下图:


添加银行卡信息

选择你的银行账户,如果你没有,点击旁边的Add Bank Account添加一个账户。
下面是添加一个账户的流程。

2-1、选择银行所在的国家
选择银行所在的国家
2-2、填写银行CNAPS Code

如果你不知道CNAPS Code是多少,可以点击Look up Transit Number来查询,查询时会根据3个关键信息来查询,如下:

  • Bank Name:银行的英文名称(不能是拼音)
  • City:银行所在的城市英文名称(中国的城市用拼音)
  • Postal Code:邮编
  • 然后在下面就会出来备选的银行,选择正确的银行后,点击next,进入下一步。


    填写银行CNAPS Code

    银行信息
2-3、确认银行信息
确认银行信息
2-4、填写银行账号信息
  • Bank Account Number:银行账号
  • Confirm Bank Account Number:再次输入银行账号
  • Account Holder Name:持卡人姓名,中文名用拼写,名在前,姓在后
  • Bank Account Currency:货币类型,一般国内的开发者选择CNY


    填写银行账号信息
2-5、确认所有信息
确认所有信息

3、填写税务信息

税务信息这一块了解不是很多,不过因为是国内开发者,可以不用太费心,税务信息分3种:

  • U.S Tax Forms: 美国税务
  • Australia Tax Forms:澳大利亚税务
  • Canada Tax Forms: 加拿大税务


    填写税务信息

    笔者选择的是U.S Tax Forms,选择后会问你两个问题,第一个问题如下:询问你是否是美国居民,有没有美国伙伴关系或者美国公司,如果没有直接选择No。


    是否美国居民

    接下来第二个问题如下:询问你有没有在美国的商业性活动,没有也直接选No。
    是否在美国的商业性活动

然后填写你的税务信息,包括以下几点:

  • Individual or Organization Name:个人或者组织名称
  • Country of incorporation: 所在国家
  • Type of Beneficial Owner:受益方式,独立开发者选个人
  • Permanent Residence:居住地址
  • Mailing address:邮寄地址
  • Name of Person Making this Declaration:声明人
  • Title:头衔

填写完这些信息后就可以提交了


提交审核

4、等待审核

当你填写完所有资料后,合同状态就会变成Processing,笔者凌晨1点左右提交,下午就通过了。


等待审核

三、添加内购买项目

App创建好之后,我们打开创建的App,在左上角选择功能,会看到左侧的App 内购买项目。我们点击右下角的加号,为App添加内购项目。


内购买项目

虚拟物品添加分为如下几种:

  1. 消耗品(Consumable products):比如游戏内金币等。
  2. 不可消耗品(Non-consumable products):简单来说就是一次购买,终身可用(用户可随时从App Store restore)。
  3. 自动更新订阅品(Auto-renewablesubscriptions):和不可消耗品的不同点是有失效时间。比如一整年的付费周刊。在这种模式下,开发者定期投递内容,用户在订阅期内随时可以访问这些内容。订阅快要过期时,系统将自动更新订阅(如果用户同意)。
  4. 非自动更新订阅品(Non-renewablesubscriptions):一般使用场景是从用户从IAP购买后,购买信息存放在自己的开发者服务器上。失效日期/可用是由开发者服务器自行控制的,而非由AppStore控制,这一点与自动更新订阅品有差异。
  5. 免费订阅品(Free subscriptions):在Newsstand中放置免费订阅的一种方式。免费订阅永不过期。只能用于Newsstand-enabled apps。

类型2、3、5都是以Apple ID为粒度的。比如小张有三个iPad,有一个Apple ID购买了不可消耗品,则三个iPad上都可以使用。

类型1、4一般来说则是现买现用。如果开发者自己想做更多控制,一般选4

之后我们会看到类型的选项,如下图


选择类型

官方的注释写的很清楚了,只在这里简单的说下前两种:

  • 消耗型项目 就像你玩游戏需要买金币,买钻石等,只要花钱就可以无限次的购买
  • 非消耗型项目 就像你在App Store购买App,买了一次之后就不用再买第二次,你拥有永久使用权。
    在我们的app中,是充值会员,所以选择的是第一种,可以无限次购买。


    填写信息

    这里有几个选项,需要填写商品名称,产品ID以及价格等级,简单说明一下

  1. 商品名称根据你的消费道具的实际意义来说明,比如“100颗宝石”,“100金币”等。
  2. 产品ID是比较重要的,由项目自定义,只要唯一即可,因为测试,我在这里随便填写的123,在实际应用中,一定要认真填写。
  3. 价格等级的话“查看价格表”中有对应的说明,可以对照着表中每个国家的货币价格与等级来选择
    接下来是语言选择,和上传快照如下图


    内置购买详细信息

    点击添加语言,填写名称和描述,这里我们依然选择简体中文,如下


    添加语言

    审核备注,根据实际情况填写,可以不填。而下面的屏幕快照,则是商品图片,以像素为单位,最低尺寸为321,390,尺寸需求如下图,上传即可。
    审核备注

    到这里为止, 我们的内购项目则添加完成。接下来则是测试阶段了。

四、申请沙盒测试账号(用来测试购买项目)

这个账号,是利用苹果的沙盒测试环境来模拟AppStore的购买流程,你肯定不会想要用真实RMB去购买测试吧?
首先我们回到iTunes Connect中,在这里我们选择用户和职能。


用户与职能

然后在上面的第三个选项沙箱技术测试员中点击加号,添加测试员。


添加测试员

在信息填写页面只简单说两句。
所有信息都可以随意填写,不用管是否真实。
App Store地区选择,一定要选对,它对应的是你创建的App的地区, 你App是中国的话, 在这里我们依然选择中国。

此账号只能用来测试,不要在正式的appstore上使用
填写完毕,点击保存后,我们则生成一个测试账号,当然这个账号是可以随时删除和添加的。


生成测试账号

五、大致流程

IAP流程分为两种,一种是直接使用Apple的服务器进行购买和验证,另一种就是自己假设服务器进行验证。由于国内网络连接Apple服务器验证非常慢,而且也为了防止黑客伪造购买凭证,通用做法是自己架设服务器进行验证。

下面我们通过图来看看两种方式的差别:

5.1、使用Apple服务器

使用Apple服务器

5.2、自己架设服务器

自己架设服务器

简单说下第二中情况的流程:

  1. 用户进入购买虚拟物品页面,App从后台服务器获取产品列表然后显示给用户
  2. 用户点击购买购买某一个虚拟物品,APP就发送该虚拟物品的productionIdentifier到Apple服务器
  3. Apple服务器根据APP发送过来的productionIdentifier返回相应的物品的信息(描述,价格等)
  4. 用户点击确认键购买该物品,购买请求发送到Apple服务器
  5. Apple服务器完成购买后,返回用户一个完成购买的凭证
  6. APP发送这个凭证到后台服务器验证
  7. 后台服务器把这个凭证发送到Apple验证,Apple返回一个字段给后台服务器表明该凭证是否有效
  8. 后台服务器把验证结果在发送到APP,APP根据验证结果做相应的处理

六、大致代码

#import "ViewController.h"
#import <StoreKit/StoreKit.h>

@interface ViewController () <SKPaymentTransactionObserver, SKProductsRequestDelegate>

@property (nonatomic, strong) NSString * receipt;

@end

@implementation ViewController

/**
 *  开发流程:
 *  1、引入头文件 #import <StoreKit/StoreKit.h>
 *  2、检查用户是否允许使用 内置购买 [SKPaymentQueue canMakePayments]
 *  3、如果允许:获取所有付费Product ID(productionIdentifier)列表。可以用常量保存在本地,也可以自己服务器返回(原因:连接苹果服务器比较慢)
 *  4、先查询是否有该productionIdentifier的商品信息,获取SKPayment实例,然后通过SKPaymentQueue的 addPayment方法发起一个购买的操作。
 *  5、监听购买结果 [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
 *  6、购买完成回调- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
 *  7、服务器验证凭证(Optional)。如果购买成功,我们需要将凭证发送到服务器上进行验证。
 *      服务器工作:
 *      7.1 接收ios端发过来的购买凭证。
 *      7.2 判断凭证是否已经存在或验证过,然后存储该凭证。
 *      7.3 将该凭证发送到苹果的服务器验证,并将验证结果返回给客户端。
 *      7.4 如果需要,修改用户相应的会员权限。
 *  8、网络异常、崩溃等导致无法验证,将购买凭证持久化。用户下次启动的时候,再发一次验证
 *      8.1 保存数据(日期、用户、凭证receipt)
 *      8.2 重新启动,读取数据,发送网络请求
 *      8.3 如果成功,删除文件
 */

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 监听购买结果
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}

- (void)buyProdutin:(UIButton *)sender
{
    // 确认用户是否允许使用 内置购买
    if([SKPaymentQueue canMakePayments]){
        // 获取商品的productionIdentifier
        NSString * productIdentifier = @"";     //用户选择
        [self getProductInfo:productIdentifier];
    }else{
        
    }
}

#pragma mark - 查询productIdentifier 的商品信息
- (void)getProductInfo:(NSString *)productIdentifier
{
    NSArray * product = [[NSArray alloc] initWithObjects:productIdentifier, nil];
    NSSet * set = [NSSet setWithArray:product];
 
    // 请求苹果服务器,是否有该商品
    SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
    request.delegate = self;
    [request start];
    
    // 这里可弹出提示信息 : 正在购买,请稍后
}

// 查询该产品信息
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    NSArray * myProduct = response.products;
    if(myProduct.count == 0){
        // 说明没有该商品,或查询失败
        
        return;
    }
    
    // 如果有,发起购买操作
    SKPayment * payment = [SKPayment paymentWithProduct:myProduct[0]];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

// 查询失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
    // 提示信息
}

#pragma mark - 购买回调
- (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;
                
            case SKPaymentTransactionStatePurchasing:
                // 商品添加进列表
                
                break;
            default:
                break;
        }
    }
}

//沙盒测试环境验证
#define SANDBOX @"https://sandbox.itunes.apple.com/verifyReceipt"
//正式环境验证
#define AppStore @"https://buy.itunes.apple.com/verifyReceipt"
/**
 *  验证购买,避免越狱软件模拟苹果请求达到非法购买问题

// 交易结束后,验证票据信息是否正确,之后才可以给用户商品
- (void)completeTransaction:(SKPaymentTransaction *)transaction {
    // 测试环境
    self.receipt = [[NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]] base64EncodedStringWithOptions:0];
    NSError *error;
    NSDictionary *requestContents = @{
                                      @"receipt-data": self.receipt
                                      };
    NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                          options:0
                                                            error:&error];
    // 设置URL
    NSURL *storeURL = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];
    NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
    [storeRequest setHTTPMethod:@"POST"];
    [storeRequest setHTTPBody:requestData];
    
   // 网络请求,验证
    
    
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

// 交易失败后,回调
- (void)failedTransaction:(SKPaymentTransaction *)transaction {
    if(transaction.error.code != SKErrorPaymentCancelled) {
        // 购买失败
        
    } else {
        // 用户取消交易
        
    }
    
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

// 已购买过商品回调
- (void)restoreTransaction:(SKPaymentTransaction *)transaction {
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

#pragma mark - 发送凭证失败的处理
/**
 *  1 保存数据(日期、用户、凭证receipt)
 *  2 重新启动,读取数据,发送网络请求
 *  3 如果成功,删除文件
 */

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

@end

需要注意以下几点:

  • 代码中的self.profuctIdArr所填写的是你的购买项目的的ID,我这里是当时填写的ID 123。
  • 在监听购买结果后,一定要调用[[SKPaymentQueue defaultQueue] finishTransaction:tran];来允许你从支付队列中移除交易。
  • 沙盒环境测试appStore内购流程的时候,请使用没越狱的设备。
  • 请务必使用真机来测试,一切以真机为准。
  • 项目的Bundle identifier需要与您申请AppID时填写的bundleID一致,不然会无法请求到商品信息。
  • 真机测试的时候,一定要退出原来的账号,才能用沙盒测试账号
  • 二次验证,请注意区分宏, 测试用沙盒验证

推荐阅读更多精彩内容