内购场景就不说了,主要方面调试测试还有审核方面吧!
基础配置填写
登录 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]) { _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; 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 { [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; } } 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 { 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 { NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL]; NSData *receiptData = [NSData dataWithContentsOfURL:recepitURL]; if (!receiptData) { [self completionStateType:CCIAPPaymentStateSuccess data:receiptData]; [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; }else { [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;
未完待续。。。
参考链接
本文标题:iOS - 苹果内购
文章作者:zerocc
发布时间:2018年10月01日
原始链接:http://www.zerocc.com.cn/20181001.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!