0. 开篇叨叨

一般稍大点的应用都会接入支付功能,对于iOS应用而言,支付渠道主要分成两类:

  • 第三方支付:

第三方支付包括:支付宝、微信、银联等支付方式,这些一般都需要有公司的形式才能接入,个人是无法接入的。

  • 应用内支付

应用内支付简称内购,IAP说的都是同一个东西,它是指在应用程序内销售虚拟商品,如果我们在App Store上销售,将收到支付金额的70%,苹果公司会抽走30%。并且需要注意的是在AS上一次性消耗商品,价格不能超过99.99美刀不然会被拒绝的。

这篇博客主要介绍比较常用的支付宝、微信以及AS内购三种支付方式,对于这块的内容大家只要记住这里的原理就可以了,各个渠道的SDK以及部分流程都会时常更新,所以不用太在意具体的差异。接入的时候还是要仔细阅读对应平台的SDK说明文档。

对于内购GitHub上面也有一些比较好的开源项目大家也可以作为借鉴:RMStore 以及 CargoBay

1. 第三方支付
1.1 支付宝平台
1.1.1 简介

支付宝接入文档地址如下:
https://docs.open.alipay.com/204/105051

在我们需要通过支付宝三方支付的时候,我们的应用调用支付宝提供的SDK,如果用户已经安装了支付宝 APP,这时候支付宝APP将会被调起来接管后续的支付流程,支付完成后跳回我们的应用展示支付结果,如果用户没有安装支付宝 APP,这时候将会调起支付宝网页支付收银台,用户登录支付宝账户,支付完后展示支付结果。

但是支付宝只针对企业或者个体户才能申请接入,对于一般的开发者是无法接入的

下面是接入支付宝的条件:

  • 申请前必须拥有经过实名认证的支付宝账户;
  • 企业或个体工商户可申请;
  • 需提供真实有效的营业执照,且支付宝账户名称需与营业执照主体一致;
  • 网站能正常访问且页面显示完整,网站需要明确经营内容且有完整的商品信息;
  • 网站必须通过ICP备案。如为个体工商户,网站备案主体需要与支付宝账户主体名称一致;
  • 如为个体工商户,则团购不开放,且古玩、珠宝等奢侈品、投资类行业无法申请本产品。
1.1.2 接入前准备

在接入前需要在支付宝开放平台创建应用,我们需要提供我们应用的基本信息,以及授权回调,加密公钥等信息,如果创建成功会获得一个APPID,通过 APPID 才能调用开放产品的接口能力。具体查看
开放平台应用创建指南

创建成功后我们就可以往我们注册的应用上添加功能,对于某些需要签约功能的应用,要先完成签约后才能使用。

为了保证交易双方的身份和数据安全,我们在调用接口前,需要配置双方密钥,并通过双方密钥对交易数据进行双方校验,支付宝使用RSA加解密技术,整个过程我们需要应用私钥(APP_PRIVATE_KEY)、应用公钥(APP_PUBLIC_KEY),以及支付宝公钥(ALIPAY_PUBLIC_KEY),我们首先会在本地生成一对应用私钥(APP_PRIVATE_KEY)、应用公钥(APP_PUBLIC_KEY),并且在上述添加应用的时候把应用公钥(APP_PUBLIC_KEY)通过开放平台给支付宝,这样我们使用应用私钥(APP_PRIVATE_KEY)加密后的数据支付宝就可以通过我们给它的应用公钥解开了。同理,支付宝那边的返回数据也可能是加密的,对于这些数据我们可以使用支付宝公钥(ALIPAY_PUBLIC_KEY)进行解密,这部分可以查看签名专区

1.1.3 支付流程

1.1.4 iOS 支付宝SDK集成

具体的集成过程可以查看集成文档

首先我们点击购买某个商品的时候会发送信息告诉我们业务服务端我们需要购买的商品信息,比如商品id等。服务端后台会生成订单ID,并将包括订单ID,订单价格等信息打包起来,在服务端使用应用私钥对这些信息加密后传递给客户端,客户端收到这个请求后直接传递给支付宝SDK发起请求,这里之所以将加密放在服务端是为了避免公私钥数据泄露,在使用的时候一定要注意构造交易数据并签名必须在商户服务端完成,商户的应用私钥绝对不能保存在商户 APP 客户端中,也不能从服务端下发。

下面是来自支付宝官网的Demo,在该Demo中为了简单它将加密放在了客户端,在实际应用的时候需要注意这一点。

//将商品信息赋予AlixPayOrder的成员变量
Order* order = [Order new];

// NOTE: app_id设置
order.app_id = appID;

// NOTE: 支付接口名称
order.method = @"alipay.trade.app.pay";

// NOTE: 参数编码格式
order.charset = @"utf-8";

// NOTE: 当前时间点
NSDateFormatter* formatter = [NSDateFormatter new];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
order.timestamp = [formatter stringFromDate:[NSDate date]];

// NOTE: 支付版本
order.version = @"1.0";

// NOTE: sign_type设置
order.sign_type = @"RSA";

// NOTE: 商品数据
order.biz_content = [BizContent new];
order.biz_content.body = @"我是测试数据";
order.biz_content.subject = @"1";
order.biz_content.out_trade_no = [self generateTradeNO]; //订单ID(由商家自行制定)
order.biz_content.timeout_express = @"30m"; //超时时间设置
order.biz_content.total_amount = [NSString stringWithFormat:@"%.2f", 0.01]; //商品价格

//将商品信息拼接成字符串
NSString *orderInfo = [order orderInfoEncoded:NO];
NSString *orderInfoEncoded = [order orderInfoEncoded:YES];
NSLog(@"orderSpec = %@",orderInfo);

// NOTE: 获取私钥并将商户信息签名,外部商户的加签过程请务必放在服务端,防止公私钥数据泄露;
// 需要遵循RSA签名规范,并将签名字符串base64编码和UrlEncode
id<DataSigner> signer = CreateRSADataSigner(privateKey);
NSString *signedString = [signer signString:orderInfo];

// NOTE: 如果加签成功,则继续执行支付
if (signedString != nil) {
//应用注册scheme,在AliSDKDemo-Info.plist定义URL types
NSString *appScheme = @"alisdkdemo";

// NOTE: 将签名成功字符串格式化为订单字符串,请严格按照该格式
NSString *orderString = [NSString stringWithFormat:@"%@&sign=%@",
orderInfoEncoded, signedString];

// NOTE: 调用支付结果开始支付
[[AlipaySDK defaultService] payOrder:orderString fromScheme:appScheme callback:^(NSDictionary *resultDic) {
NSLog(@"reslut = %@",resultDic);
}];
}

处理的结果分成两种方式返回:

  • 同步通知:同步通知指的是支付宝SDK对支付请求处理完毕后将结果返回给商户App端
  • 异步通知:异步通知指的是支付宝SDK处理完结果后将结果通过异步通知地址notify_url,通过POST方式将结果回传给我们业务后台。

同步返回的数据,只是一个简单的结果通知,商户确定该笔交易付款是否成功需要依赖服务端收到支付宝异步通知的结果进行判断。具体返回的结果可以查看通知参数说明

同时不要忘记在info.plist 注册的 scheme。接下来就是在AppDelegate中处理支付宝返回的处理结果,这个回调会在这笔交易被买家支付成功后支付宝收银台上显示该笔交易成功,并提示用户“返回”的时候被调用。

- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation {

if ([url.host isEqualToString:@"safepay"]) {
//跳转支付宝钱包进行支付,处理支付结果
[[AlipaySDK defaultService] processOrderWithPaymentResult:url standbyCallback:^(NSDictionary *resultDic) {
//在这里处理对应的支付结果
}];
}
return YES;
}

// NOTE: 9.0以后使用新API接口
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString*, id> *)options
{
if ([url.host isEqualToString:@"safepay"]) {
//跳转支付宝钱包进行支付,处理支付结果
[[AlipaySDK defaultService] processOrderWithPaymentResult:url standbyCallback:^(NSDictionary *resultDic) {
//在这里处理对应的支付结果
}];
}
return YES;
}

resultDic整个结构如下所示:

{
"memo" : "xxxxx",
"result" : "{
\"alipay_trade_app_pay_response\":{
\"code\":\"10000\",
\"msg\":\"Success\",
\"app_id\":\"2014072300007148\",
\"out_trade_no\":\"081622560194853\",
\"trade_no\":\"2016081621001004400236957647\",
\"total_amount\":\"0.01\",
\"seller_id\":\"2088702849871851\",
\"charset\":\"utf-8\",
\"timestamp\":\"2016-10-11 17:43:36\"
},
\"sign\":\"NGfStJf3i3ooWBuCDIQSumOpaGBcQz+aoAqyGh3W6EqA/gmyPYwLJ2REFijY9XPTApI9YglZyMw+ZMhd3kb0mh4RAXMrb6mekX4Zu8Nf6geOwIa9kLOnw0IMCjxi4abDIfXhxrXyj********\",
\"sign_type\":\"RSA2\"
}",
"resultStatus" : "9000"
}

整个交互流程如下图所示:

下图是支付失败时候的交互流程图:

对应的错误码可以查看官方的错误文档

商户系统接收到通知以后,必须通过验签来确保支付通知是由支付宝发送的。
除了正常的支付交易流程外,支付宝也提供交易查询、关闭、退款、退款查询以及对账等配套API详细可以查看官方文档。

1.1.5 调试上线

在上线之前可以在沙箱环境开发联调,联调通过后就可以在支付宝上线应用了,只有上线状态下的应用才能够调用生产环境的接口,在申请上线之前建议认真对照[开放平台第三方应用安全开发指南](https://docs.open.alipay.com/common/105912),避免审核不通过被打回来。

1.2 微信支付

微信支付目前也是仅接受公司主体的移动应用申请微信APP支付权限,目前手续费和支付宝一样都是0.6%到1%。具体的概述见微信APP支付接入商户服务中心

下面是微信支付的整个交互时序图:

1.2.1
  • 申请Appid

和支付宝支付类似在接入SDK之前需要到微信开放平台申请开发APP应用之后会返回一个唯一标识APPID:

在URL Types中中添加一项URL Schemes为刚刚申请的Appid的项。

  • 导入WechatOpenSDK

通过CocoaPods导入SDK

pod 'WechatOpenSDK'
  • 导入WXApi

import WXApi.h 头文件,并增加 WXApiDelegate 协议。


#import <UIKit/UIKit.h>
#import "WXApi.h"

@interface AppDelegate : UIResponder<UIApplicationDelegate, WXApiDelegate>

@property (strong, nonatomic) UIWindow *window;

@end

  • 向微信注册应用

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
    //向微信注册
    [WXApi registerApp:@"wxd930ea5d5a258f4f"];
    return YES;
    }
  • 重写AppDelegate的handleOpenURL和openURL方法:

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {   
return [WXApi handleOpenURL:url delegate:self];
}

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
return [WXApi handleOpenURL:url delegate:self];
}
  • 接下来需要实现WXApiDelegate协议的两个方法:
-(void) onReq:(BaseReq*)reqonReq

这个是是微信终端向我们应用发起请求的回调,我们调用完后必须调用sendRsp返回。

-(void) onResp:(BaseResp*)resp

如果我们应用向微信发送了sendReq的请求,那么onResp会被回调。`

如果我们的应用需要发送消息给微信,那么需要调用
[4] 如果你的程序要发消息给微信,那么需要调用WXApi的sendReq函数:

调用WXApi的sendReq函数

-(BOOL) sendReq:(BaseReq*)req

下面是接入微信支付的文档,遇到问题的时候可以查阅相关文档。

微信支付开发文档

微信支付开放平台

2. 应用内购 IAP

在介绍应用内购之前需要明确下哪些商品是属于内购范围,应用内购主要是指购买应用内需要付费使用的产品功能或虚拟商品/服务,但是那些实体商品或者购买来不是在应用内部使用的虚拟商品都不是IAP所涵盖的范围,苹果有明确规定在应用内适用的虚拟商品或者服务必须使用IAP进行购买支付,不允许使用包括Apple Pay在内的其它第三方支付,也不允许以任何方式引导用户通过应用外渠道购买。

应用内购准备工作主要包括三个部分:

  • 配置内购商品
  • 填写银行卡信息
  • 配置沙盒账号
2.1 App,商品信息,银行卡信息,测试账号配置

首先进入Itunes Connect网站,在这之前需要确保你已经拥有了个人开发者账号,公司开发者账号,但是不能是企业级开发者账号,并且公司开发者账号下的Member权限不能进入。

* 我的App主要用于管理自己的应用比如应用信息编辑,上架,下架操作。
* 销售和趋势主要是来查看App在各个平台的下载量,收入等方面数据。
* 付款和财务报告显示的是我们的收入以及付款等相关信息。
* 用户和职能用于生成相应账号,例如苹果沙河测试账号。
* 协议,税务和银行业务则是我们银行相关账户的信息设置。

商品信息配置

创建应用在之前的《iOS 上架流程》中已经介绍过了,这里就不重复了,在创建完应用后进入下面的页面:

点击加号就可以准备添加内购商品了。

在AppStore中售卖的商品可以分成如下几类:消耗型,非消耗型,自动续期订购,非续期订购这几种,详细说明可以见下图:

填写对应的内购项目信息:

产品 ID 必须具有唯一性,通常我们使用Bundle Identidier作为前缀,后面拼接上唯一的商品名或者 ID,这里需要注意的是一旦建立一个内购商品,那么它的产品ID将永远被占用,即使产品被删除也会一直存在,所以配置的时候要格外慎重。另外还需要注意价格等级这个概念,内购商品的定价只能从苹果提供的价格等级去选择,这个价格等级是固定的,同一价格等级会对应各个国家的货币,内购商品的价格是根据 Apple ID 所在区域的货币进行结算。

最后还可以配置App Store推广的功能。这样用户就可以在App Store 内 App 的下载页面内直接购买应用的内购商品

银行卡信息配置

点击协议,税务和银行业务,进行银行卡信息配置。这里我们需要申请的是iOS Paid Application合同,这里主要有Contract Info(联系信息),Bank Info(银行信息),Tax Info(税务信息)三大块。这部分配置可以参照iOS内购一条龙——账户信息填写来配置。

沙盒测试账号配置

沙盒在之前的支付宝,微信支付也提到过,为什么需要沙盒测试?因为如果我们用正式账号进行测试的话,即便是自己购买自己的东西,也会有30%的利润进入苹果的口袋,这对于测试那些比较贵的商品是很致命的,苹果为了解决这个问题,提供了一套沙盒体系,在沙盒里面,用户用的是沙盒账号,走的是沙盒接口,购买后的费用会原样退回。

从用户和职能入口进入添加沙盒测试的入口。


这里需要注意的是,电子邮件不能是已经注册过AppleId的邮箱,选择的地区会影响到结算的价格,如果需要测试多个不同地区的情况,可以申请多个不同地区的账号测试。沙盒测试不支持直接从AS下载的安装包,必须使用发布测试的ad hoc 证书或者Develop 证书签名过后的包,并且使用真机环境下进行测试。

在开始测试的时候先退出真机的App Store的真实Apple ID 账号,退出之后不需要在App Store 里面登录沙箱测试账号,然后去 App 里面测试购买商品,会弹出登录框,选择“使用现有的 Apple ID”然后登录沙箱测试账号,登录成功之后会弹出购买提示框,点击购买,然后会弹出提示框完成购买,弹窗上面有表明当前的环境是沙盒环境。

Xcode设置相关


2.2 IAP 流程

上面介绍了支付宝的交易流程,这里来看下IAP 流程。

  1. 首先我们进入购买页面的时候会从我们自己的服务器那边拉取Product ID列表。
  2. 拿到服务器返回的Product ID列表后会将这个列表发送到App Store获取用于展示的商品信息。
  3. 用户选中要购买的物品,这时候会向App Store发送一个交易请求。
  4. App Store 查询该交易的有效性后会通过delegte回调应用的方法,在这个方法中,应用将交易添加到交易队列从而发起交易,同时由于添加了交易Observer所以App Store交易状态改变的时候会通过Observer通知应用。
  5. App Store 调起弹窗让用户确认购买,输入密码,苹果服务器验证用户请求并从用户帐号扣款,一旦交易成功就会回调App, 通知购买成功,并把收据数据(里面记录了本次交易的证书和签名信息)写入到APP沙盒中。
  6. APP从沙盒中获取服务器下发的收据信息,并将收据信息上传给业务服务器。(其实这里既可以直接在 App 端验证也可以让服务器去验证,但是现在大多数是放在后台去做这一步校验)
  7. 业务服务器在收到客户端上传上来的收据数据后,会去App Store检查收据的有效性,验证成功之后在后台收据需要和自己的订单号进行映射并且记录在数据库,之后每次验证之前都需要先判断收据是否存在,防止App端重复上传相同的收据,重复发放内购商品,这之后通知客户端,同时发放商品,客户端收到结果后将该交易从支付队列中移除。

在获取商品列表的时候如果遇到获取不到商品信息的情况的话,可以从如下几个方面进行排查:

  1. 确定内购商品是否添加到正确的App中,虽然很少会犯这个错误,但是还是需要检查下。
  2. 使用非越狱的真机进行测试,看下是否能够拉取到。
  3. 后台配置的App Bundle ID是否和当前应用的App Bundle ID 一致。
2.3 相关代码
2.3.1 导入相关库

支付需要用到StoreKit.framework,在开始的时候需要导入到项目中。

#import <Foundation/Foundation.h>

typedef enum {
IDLIAPPurchSuccess = 0, // 购买成功
IDLIAPPurchFailed = 1, // 购买失败
IDLIAPPurchCancle = 2, // 取消购买
IDLIAPPurchVerFailed = 3, // 订单校验失败
IDLIAPPurchVerSuccess = 4, // 订单校验成功
IDLIAPPurchNotArrow = 5, // 不允许内购
} IDLIAPPurchStatus;

typedef void (^IDLIAPCompletionHandle)(IDLIAPPurchStatus status, NSData *data);

@interface IDLIAPManager : NSObject

+ (instancetype)shareManager;

- (void)purchWithID:(NSString *)purchID completeHandle:(IDLIAPCompletionHandle)handle;

@end


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

@interface IDLIAPManager()< SKPaymentTransactionObserver , SKProductsRequestDelegate >

@property(nonatomic, copy , readwrite) IDLIAPCompletionHandle completeHandler;
@property(nonatomic, strong, readwrite) NSString *purchID;

@end


@implementation IDLIAPManager

#pragma mark - Singleton
+ (instancetype)shareManager{
static IDLIAPManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
manager = [[IDLIAPManager alloc] init];
});
return manager;
}

#pragma mark - Initializer
- (instancetype)init{
if (self = [super init]) {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}

#pragma mark - Dealloc
- (void)dealloc{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

#pragma mark - Public
- (void)purchWithID:(NSString *)purchID completeHandle:(IDLIAPCompletionHandle)handle{
if (purchID) {
//先检查是否有付款权限
if ([SKPaymentQueue canMakePayments]) {
self.purchID = purchID;
self.completeHandler = handle;
//使用productIdentifiers 生成 SKProductsRequest
NSSet *productIdentifiers = [NSSet setWithArray:@[purchID]];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
request.delegate = self;
[request start];
}else{
//没有权限购买
[self handleActionWithStatus:IDLIAPPurchNotArrow data:nil];
}
}
}

#pragma mark - Private

- (void)handleActionWithStatus:(IDLIAPPurchStatus)status data:(NSData *)data{
if(_completeHandler){
_completeHandler(status,data);
}
}

#pragma mark - SKProductsRequestDelegate

//[request start] 通过这个回调返回,这里会返回配置的商品信息
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
NSArray *product = response.products;
if([product count] <= 0){
return;
}

SKProduct *p = nil;
for(SKProduct *pro in product){
if([pro.productIdentifier isEqualToString:_purchID]){
p = pro;
break;
}
}
SKPayment *payment = [SKPayment paymentWithProduct:p];
[[SKPaymentQueue defaultQueue] addPayment:payment]; //这里发起一个购买的操作
}

//如果交易成功会通过之前监听的交易通知到这里
#pragma mark - SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
for (SKPaymentTransaction *tran in transactions) {
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
[self completeTransaction:tran];
break;
case SKPaymentTransactionStatePurchasing:
break;
case SKPaymentTransactionStateRestored:
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:tran];
break;
default:
break;
}
}
}

- (void)completeTransaction:(SKPaymentTransaction *)transaction{

NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];

if (receiptData > 0) {
// 向自己的服务器验证购买凭证
// 如果校验成功调用[[SKPaymentQueue defaultQueue] finishTransaction:transaction]; 结束交易
}
}

- (void)failedTransaction:(SKPaymentTransaction *)transaction{
if (transaction.error.code != SKErrorPaymentCancelled) {
[self handleActionWithStatus:SIAPPurchFailed data:nil];
}else{
[self handleActionWithStatus:IDLIAPPurchCancle data:nil];
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
@end

为了不导致整个文章篇幅太长上面只是IAP的简要代码,真正的流程包括重试机制等。后续会把这部分代码整理下放到github上。

2.4 内购过程中可能会遇到的问题

这部分大家可以参阅:

Contents
  1. 1. 0. 开篇叨叨
  2. 2. 1. 第三方支付
  3. 3. 1.1 支付宝平台
    1. 3.1. 1.1.1 简介
    2. 3.2. 1.1.2 接入前准备
    3. 3.3. 1.1.3 支付流程
    4. 3.4. 1.1.4 iOS 支付宝SDK集成
    5. 3.5. 1.1.5 调试上线
  4. 4. 1.2 微信支付
  5. 5. 1.2.1
  6. 6. 2. 应用内购 IAP
    1. 6.1. 2.1 App,商品信息,银行卡信息,测试账号配置
    2. 6.2. 2.2 IAP 流程
    3. 6.3. 2.3 相关代码
    4. 6.4. 2.3.1 导入相关库
    5. 6.5. 2.4 内购过程中可能会遇到的问题