内购场景就不说了,主要方面调试测试还有审核方面吧!

基础配置填写

登录 storeconnect ( https://appstoreconnect.apple.com ) 网站 ,进行相关配置。

1. 协议、税务和银行业务 信息填写
协议、税务和银行业务

详细过程就不写了很多。

2. 创建内购产品

点击进入我的 App -> 功能 创建添加内购商品,内购产品类型有四种:

![创建内购商品](iOS-苹果内购/创建内购商品.png)
  • 消耗型商品: 如类似游戏中的钻石,被消耗的去购买其它,要选择消耗型商品;
  • 非消耗型商品: 如看一部电影需要付2块钱的,无法被消耗的商品,付款后直接得到相应东西,非消耗型消耗一次后在该 APP ID 下都能使用。
  • 自动续期订阅类型商品: 。。。
  • 非续期订阅类型商品: 。。。

注意:当APP中有过订阅类型商品,注意是有过,创建过再删除也算,这个APP无法被转移账号;

创建完成后就是审核内购商品了略。

3. 添加沙盒测试账号
创建内购商品

注意:使用的邮箱必须是新的,不能是已绑定或注册过的苹果账号;

内购核心代码

1. 正常内购业务简易流程

  • 1.1 请求 App Service 创建订单,返回给移动端;
  • 1.2 App 拿到交易信息,调起 IAP 服务发送交易请求(iOS api), 回调代理中将购买的商品添加进购买队列;
  • 1.3 用户确认购买,输入密码确认交易后,IAP 服务通知 App 返回交易结果(IAP 票据信息);
  • 1.4 App 将获得的 IAP 票据信息上报 App Service;
  • 1.5 App Service 用得到的票据信息请求 IAP Service 进行票据校验;
  • 1.6 IAP Service 将校验结果返回告知给 App Service;
  • 1.7 App Service 将票据校验结果返回告诉 App;

交易步骤流程图如下:
内购流程

2. 核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef NS_OPTIONS(NSInteger, CCIAPPaymentState) {
CCIAPPaymentStateSuccess, // 购买成功
CCIAPPaymentStateFailed, // 购买失败
CCIAPPaymentStateCancle, // 取消购买
CCIAPPaymentStateNotArrow, // 不允许内购
CCIAPPaymentStateOrderAppStoreLose,
CCIAPPaymentStateOrderVerifyFailed
};

typedef void (^PurchaseCompletion)(CCIAPPaymentState stateType,NSData *data);
typedef void (^OrderLoseHandler)(CCIAPPaymentState stateType,NSData *data);

@interface CCIAPManager : NSObject
+ (instancetype)shareCCIAPManager;

- (void)purchaseWithProductID:(NSString *)productID purchaseCompletion:(PurchaseCompletion)completion;

- (void)gainLoseOrderHandler:(OrderLoseHandler)handler;

@end

NS_ASSUME_NONNULL_END


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

@interface CCIAPManager ()<SKPaymentTransactionObserver,SKProductsRequestDelegate>
@property (nonatomic, copy) NSString *productID;
@property (nonatomic, copy) PurchaseCompletion completion;
@property (nonatomic, copy) OrderLoseHandler handler;

@end

@implementation CCIAPManager

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

+ (instancetype)shareCCIAPManager {
static CCIAPManager *IAPManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
IAPManager = [[CCIAPManager alloc] init];
});

return IAPManager;
}

- (instancetype)init {
self = [super init];
if (self) {
// 开始支付事务监听, 并且开始支付凭证验证队列.
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}

return self;
}

- (void)completionStateType:(CCIAPPaymentState)state data:(NSData *)data {
if (_completion) {
_completion(state,data);
}
}

- (void)orderHandlerStateType:(CCIAPPaymentState)state data:(NSData *)data {
if (_handler) {
_handler(state,data);
}
}

- (void)purchaseWithProductID:(NSString *)productID purchaseCompletion:(PurchaseCompletion)completion {
if (productID) {
if ([SKPaymentQueue canMakePayments]) {
// 1. 开始请求购买
_productID = productID;
_completion = completion;
NSSet *productSet = [NSSet setWithArray:@[productID]];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:productSet];
request.delegate = self;
[request start];

}else {
[self completionStateType:CCIAPPaymentStateNotArrow data:nil];
}

}
}

- (void)gainLoseOrderHandler:(OrderLoseHandler)handler {
if ([SKPaymentQueue canMakePayments]) {
NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
// 检测是否有未完成的交易(因为有获得的transactions为空的情况)
if (transactions.count > 0) {
SKPaymentTransaction* transaction = [transactions firstObject];
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:recepitURL];
if (!receiptData) { // 发送服务器校验订单,如果为空获取不到则出现掉单情况
[self orderHandlerStateType:CCIAPPaymentStateSuccess data:receiptData];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}else { // 还是获取不到服务器去手动补单吧 finish掉 (appStore 返回掉单情况)
[self orderHandlerStateType:CCIAPPaymentStateOrderAppStoreLose data:nil];
}

[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}
}else {
[self orderHandlerStateType:CCIAPPaymentStateNotArrow data:nil];
}
}

#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
NSArray *product = response.products;
if([product count] > 0){ // 存在商品
SKProduct *p = nil;
for(SKProduct *pro in product){
if([pro.productIdentifier isEqualToString:_productID]){
p = pro;

break;
}
}

// 2. 将购买的商品添加进购买队列
SKPayment *payment = [SKPayment paymentWithProduct:p];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}

NSLog(@"productID:%@", response.invalidProductIdentifiers);
NSLog(@"产品付费数量:%lu",(unsigned long)[product count]);
}

#pragma mark - SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
// 3. 输入密码确认交易后苹果返回购买回调结果
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing: //正在交易

break;
case SKPaymentTransactionStatePurchased:
[self transactionPurchased:transaction];

break;
case SKPaymentTransactionStateRestored: //恢复交易

[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed: //交易失败
[self transactionFailed:transaction];

break;
case SKPaymentTransactionStateDeferred: // 交易状态未确定

break;

default:
break;
}
}
}

#pragma mark - transactionState

// 交易中.
- (void)transactionPurchasing:(SKPaymentTransaction *)transaction {
NSLog(@"交易中...");
}

// 交易成功.
- (void)transactionPurchased:(SKPaymentTransaction *)transaction {
// 4. 获取交易成功的票据
NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:recepitURL];

// 发送服务器校验订单,如果为空获取不到则出现掉单情况
if (!receiptData) {
[self completionStateType:CCIAPPaymentStateSuccess data:receiptData];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}else { // 一定是掉单了 (appStore 返回掉单情况)
[self completionStateType:CCIAPPaymentStateOrderAppStoreLose data:nil];
}
}

// 交易失败.
- (void)transactionFailed:(SKPaymentTransaction *)transaction {
if (transaction.error.code != SKErrorPaymentCancelled) {
[self completionStateType:CCIAPPaymentStateFailed data:nil];
}else {
[self completionStateType:CCIAPPaymentStateCancle data:nil];
}

[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

// 已经购买过该商品.
- (void)transactionRestored:(SKPaymentTransaction *)transaction {
NSLog(@"已经购买过该商品...");
}

// 交易延期.
- (void)transactionDeferred:(SKPaymentTransaction *)transaction {
NSLog(@"交易延期...");
}

@end

3. 注意事项

  • 测试内购必须先在 itune Store 与 App Store 里退出自己的 Apple ID 登录沙箱帐号,否则点击购买会重复弹出 Apple ID 登录框一直让你继续输入;
  • 项目的Bundle identifier需要与您申请 AppID 时填写的 bundleID 一致,不然会无法请求到商品信息;
  • App Service 校验票据过程区分沙盒环境还是生产环境通过返回 Status Code 决定是否去沙盒进行验证,验证的顺序先验证正式环境,此时若返回值为 21007,就需要去沙盒环境下的校验;
  • 在监听购买结果后,一定要调用 [[SKPaymentQueue defaultQueue] finishTransaction:tran]; 来允许你从支付队列中移除交易,否则下次购买无法购买 IAP 支付队列被阻塞一直是处于 purchasing 的状态;
  • 关于交易事务执行 finishTransaction: 时机问题,很多是等服务器告知 App 校验成功后再进行 finish 操作这样处理,这样好处就是只有没有执行 finish 操作,再次购买就会调用以前已经购买成功的交易事务去购买因为已经购买过如下图弹窗,这是苹果一个自带的补单机制,下次调用 [[SKPaymentQueue defaultQueue] addTransactionObserver:self]后会再次回调(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions方法,这种处理会导致一个问题无法对应到之前 App Service 自身创建的订单号,还有一些例如游戏开服的一些限时活动也无法多次购买(等待服务器结果再finish)服务器压力大等;
    创建内购商品
  • 我的处理方式是只要拿到 IAP 的票据信息就执行 finishTransaction: 操作,这样不会阻塞交易事务队列,如果后续步骤失败则将 App Service 自身创建的订单号和 IAP 的票据信息一起缓存再进行其它时机上报服务器处理掉单(如果服务器在失败后续处理了则本地缓存删除否则服务器重新检验票据);

4. 丢单处理

苹果自身问题导致的掉单(未执行 finishTransaction):

  • 1.3 步骤中交易成功后,获取不到 IAP 的票据信息(receiptData),弱网情况容易复现这种情况,这是一定是丢单了,通过调用方法 (void)gainLoseOrderHandler:(OrderLoseHandler)handler 其它时机重新获取票据再上报 App Service,或者服务器手动补单通过创建订单时间日期去相应补单(一般补单脚本);

App Service 问题导致的掉单(已经执行 finishTransaction):

  • 1.4 步骤中将获取到的 IAP 的票据信息上传服务器失败,则出现掉单用户无法获取相应的道具,这个时候应该将 IAP 的票据信息和App Service 创建的订单号 和 用户信息一起缓存到本地,最好使用 keychain 进行缓存;
  • 1.6步中 或者 1.7 步骤中异常失败(可能是网络情况导致也有可能是弹窗支付完成后用户关闭了 App 等情况),其导致的可能是 App Service 请求 IAP Service 失败情况或者 App Service 返回 App 失败,也应该进行如上缓存;再在其它合适时机上报 IAP 的票据信息给 App Service;

未完待续。。。

参考链接