官方文檔In-App Purchase
內(nèi)購(gòu)的前期準(zhǔn)備等工作本文不講述,有需要的可以查看網(wǎng)上其他文章,不少講的挺詳細(xì)的。
首先Xcode里的Capabilities中的In-App Purchase的能力打開(kāi), 如下圖

關(guān)于內(nèi)支付的文章網(wǎng)上很多,解決掉單問(wèn)題的文章及方案也是一搜一大堆,但是文章中講的掉單解決方案的實(shí)施難易程度以及可行性或者說(shuō)是否適合你的產(chǎn)品就需要你自己做好判斷了。我寫(xiě)這篇文章一方面總結(jié)下之前自身解決內(nèi)購(gòu)掉單的經(jīng)驗(yàn)另外希望能幫得到需要的人就更好了。
上代碼前先講一下我們產(chǎn)品的充值流程:
app端根據(jù)用戶選擇的商品ID發(fā)起請(qǐng)求(請(qǐng)求蘋(píng)果后臺(tái)商品) -> 請(qǐng)求回調(diào)中找到與剛商品的ID一致的產(chǎn)品然后發(fā)送購(gòu)買(mǎi)請(qǐng)求 -> 監(jiān)聽(tīng)購(gòu)買(mǎi)結(jié)果回調(diào)中狀態(tài)為SKPaymentTransactionStatePurchased(即交易完成)時(shí),調(diào)用自己服務(wù)接口將蘋(píng)果回調(diào)給的憑據(jù)傳給服務(wù)端 -> 服務(wù)端驗(yàn)證憑據(jù)成功后將用戶充值的商品分發(fā)給該賬戶下
對(duì)于內(nèi)購(gòu),我寫(xiě)了一個(gè)單例(建議單例,保證全局只有一個(gè)內(nèi)購(gòu)監(jiān)聽(tīng))
單例.h文件:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface IWRechargeTool : NSObject
+ (instancetype)sharedInstance;
/**
掉單處理
*/
- (void)checkIAPHandle;
/**
內(nèi)購(gòu)
@param goodsId 商品ID
*/
- (void)iapHandleWithGoodsId:(NSString *)goodsId;
@end
NS_ASSUME_NONNULL_END
.m文件
內(nèi)購(gòu)StoreKit肯定是要添加的
#import <StoreKit/StoreKit.h>
添加內(nèi)購(gòu)監(jiān)聽(tīng)與代理:
SKPaymentTransactionObserver, SKProductsRequestDelegate
- 這里添加observerExist只是為了確保監(jiān)聽(tīng)始終只有一個(gè),loading為了確保loading的顯示
static IWRechargeTool *tool = nil;
@interface IWRechargeTool ()<SKPaymentTransactionObserver, SKProductsRequestDelegate>
@property (nonatomic, copy) NSString *goodsId;
@property (nonatomic, assign) BOOL observerExist;//觀察是否存在 YES(存在) NO(不存在 默認(rèn))
@property (nonatomic, assign) BOOL loading;//loading是否存在 YES(存在) NO(不存在 默認(rèn))
@end
+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
tool = [[IWRechargeTool alloc] init];
});
return tool;
}
- (instancetype)init
{
if (self = [super init]) {
// 3.設(shè)置支付服務(wù) 監(jiān)聽(tīng)
NSLog(@"==tool init");
self.observerExist = NO;
self.loading = NO;
[self addIAPObserverHandle];
}
return self;
}
請(qǐng)不要吐槽單例寫(xiě)法_
這個(gè)方法可以理解為用戶選擇了某一商品并且點(diǎn)擊了購(gòu)買(mǎi)按鈕:
- (void)iapHandleWithGoodsId:(NSString *)goodsId
{
NSLog(@"內(nèi)支付開(kāi)始: goodsId: %@", goodsId);
if (StrEmpty(goodsId)) {
return;
}
self.goodsId = goodsId;
[self addLoadingHandle];
[self addIAPObserverHandle];
// 5.點(diǎn)擊按鈕的時(shí)候判斷app是否允許apple支付
//如果app允許applepay
if ([SKPaymentQueue canMakePayments]) {
NSLog(@"==canMakePayments");
NSLog(@"==goodsId: %@", self.goodsId);
// 6.請(qǐng)求蘋(píng)果后臺(tái)商品
[self getRequestAppleProduct];
} else {
NSLog(@"not canMakePayments");
[self removeLoadingHandle];
[self removeIAPObserverHandle];
[MBProgressHUD showToast:@"請(qǐng)打開(kāi)Apple支付"];
}
}
//請(qǐng)求蘋(píng)果商品
- (void)getRequestAppleProduct
{
NSLog(@"====請(qǐng)求蘋(píng)果商品");
// 7.這里的goodId就對(duì)應(yīng)著蘋(píng)果后臺(tái)的商品ID,他們是通過(guò)這個(gè)ID進(jìn)行聯(lián)系的。
NSArray *product = [[NSArray alloc] initWithObjects:self.goodsId, nil];
NSSet *nsset = [NSSet setWithArray:product];
// 8.初始化請(qǐng)求
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
// 9.開(kāi)始請(qǐng)求
[request start];
}
// 10.接收到產(chǎn)品的返回信息,然后用返回的商品信息進(jìn)行發(fā)起購(gòu)買(mǎi)請(qǐng)求
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSLog(@"無(wú)效商品列表: %@", response.invalidProductIdentifiers);
NSArray *product = response.products;
//如果服務(wù)器沒(méi)有產(chǎn)品
if([product count] == 0){
NSLog(@"nothing");
[self removeLoadingHandle];
[self removeIAPObserverHandle];
[MBProgressHUD showToast:@"沒(méi)有有效商品"];
return;
}
NSLog(@"====product count: %lu", (unsigned long)product.count);
SKProduct *requestProduct = nil;
for (SKProduct *pro in product) {
NSLog(@"%@", [pro description]);
NSLog(@"%@", [pro localizedTitle]);
NSLog(@"%@", [pro localizedDescription]);
NSLog(@"%@", [pro price]);
NSLog(@"%@", [pro productIdentifier]);
// 11.如果后臺(tái)消費(fèi)條目的ID與我這里需要請(qǐng)求的一樣(用于確保訂單的正確性)
if([pro.productIdentifier isEqualToString:self.goodsId]){
[self addLoadingHandle];
[self addIAPObserverHandle];
requestProduct = pro;
// 12.發(fā)送購(gòu)買(mǎi)請(qǐng)求
SKPayment *payment = [SKPayment paymentWithProduct:requestProduct];
[[SKPaymentQueue defaultQueue] addPayment:payment];
NSLog(@"goodsId: %@", self.goodsId);
NSLog(@"======addPayment");
break;
}
}
}
// 13.監(jiān)聽(tīng)購(gòu)買(mǎi)結(jié)果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
NSLog(@"==監(jiān)聽(tīng)購(gòu)買(mǎi)結(jié)果==");
[self addLoadingHandle];
[self addIAPObserverHandle];
for(SKPaymentTransaction *tran in transactions){
NSLog(@"==%@", tran.payment.productIdentifier);
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
NSLog(@"交易完成");
[self completeTransaction:tran];
break;
case SKPaymentTransactionStatePurchasing:
NSLog(@"商品添加進(jìn)列表=正在購(gòu)買(mǎi)");
[MBProgressHUD showToast:@"正在購(gòu)買(mǎi)"];
break;
case SKPaymentTransactionStateRestored:
NSLog(@"已經(jīng)購(gòu)買(mǎi)過(guò)商品");
[self removeLoadingHandle];
[self removeIAPObserverHandle];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
break;
case SKPaymentTransactionStateFailed:
NSLog(@"交易失敗");
[self removeLoadingHandle];
[self removeIAPObserverHandle];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
[MBProgressHUD showToast:@"交易失敗"];
break;
case SKPaymentTransactionStateDeferred:
NSLog(@"==還在隊(duì)列里 狀態(tài)還未決定");
[MBProgressHUD showToast:@"正在購(gòu)買(mǎi)..."];
break;
default:
NSLog(@"==updatedTransactions default");
break;
}
}
}
- 無(wú)論前端是否驗(yàn)證返回的憑據(jù),后臺(tái)均需要驗(yàn)證,因此前端我選擇不驗(yàn)證憑據(jù)
// 14.交易結(jié)束,當(dāng)交易結(jié)束后還要去appstore上驗(yàn)證支付信息是否都正確,只有所有都正確后,我們就可以給用戶方法我們的虛擬物品了。
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
// 驗(yàn)證憑據(jù),獲取到蘋(píng)果返回的交易憑據(jù)
// appStoreReceiptURL iOS7.0增加的,購(gòu)買(mǎi)交易完成后,會(huì)將憑據(jù)存放在該地址
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
// 從沙盒中獲取到購(gòu)買(mǎi)憑據(jù)
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
/**
20 BASE64 常用的編碼方案,通常用于數(shù)據(jù)傳輸,以及加密算法的基礎(chǔ)算法,傳輸過(guò)程中能夠保證數(shù)據(jù)傳輸?shù)姆€(wěn)定性
21 BASE64是可以編碼和解碼的
22 */
NSString *encodeStr = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
[self getApplePayDataToServerRequsetWith:transaction sendString:encodeStr];
}
注意如下的[[SKPaymentQueue defaultQueue] finishTransaction:transaction] 與發(fā)送購(gòu)買(mǎi)請(qǐng)求的[[SKPaymentQueue defaultQueue] addPayment:payment]必須確保成對(duì)出現(xiàn),若有未結(jié)束掉的,每次添加內(nèi)購(gòu)監(jiān)聽(tīng),回調(diào)結(jié)果中均會(huì)將未結(jié)束掉的事務(wù)回調(diào)到app端;
解決掉單也是依據(jù)該機(jī)制來(lái)處理的
- (void)getApplePayDataToServerRequsetWith:(SKPaymentTransaction *)transaction sendString:(NSString *)sendString{
NSLog(@"==========getApplePayDataToServerRequsetWith=========");
NSLog(@"==憑據(jù): %@", sendString);
NSMutableDictionary *parms = [NSMutableDictionary dictionary];
/*
receipt String 是 蘋(píng)果支付后返回的簽名字符串 -
*/
parms[@"receipt"] = sendString;
WEAKSELF
[IWNetworkManager postDataWithUrl:kPayApplePay_update parameters:parms type:IWLoadingTypeAll activityInView:nil alertMessage:nil success:^(id response, NSInteger resultCode, NSString *resultMessage) {
if (resultCode == k200) {
[[NSNotificationCenter defaultCenter] postNotificationName:kUpdateUserInfoNotification object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:kMissingOrderHandleNotification object:nil];
[weakSelf removeLoadingHandle];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[weakSelf removeIAPObserverHandle];
[MBProgressHUD showToast:@"購(gòu)買(mǎi)成功"];
} else {
[weakSelf removeLoadingHandle];
[MBProgressHUD showToast:@"購(gòu)買(mǎi)失敗"];
}
} failure:^(NSError *error) {
[weakSelf removeLoadingHandle];
[MBProgressHUD showToast:@"購(gòu)買(mǎi)失敗, 請(qǐng)重啟App等待數(shù)秒或聯(lián)系客服"];
}];
}
//請(qǐng)求失敗
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
[self removeLoadingHandle];
[self removeIAPObserverHandle];
NSLog(@"error: %@", error);
[MBProgressHUD showToast:@"支付請(qǐng)求失敗"];
}
//反饋請(qǐng)求的產(chǎn)品信息結(jié)束后
- (void)requestDidFinish:(SKRequest *)request{
NSLog(@"信息反饋結(jié)束");
}
- (void)removeIAPObserverHandle
{
NSLog(@"removeIAPObserverHandle");
if (self.observerExist) {
NSLog(@"==removeIAPObserverHandle");
self.observerExist = NO;
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
}
- (void)addIAPObserverHandle
{
NSLog(@"addIAPObserverHandle");
if (!self.observerExist) {
NSLog(@"==addIAPObserverHandle");
self.observerExist = YES;
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
}
- (void)removeLoadingHandle
{
NSLog(@"removeLoadingHandle");
if (self.loading) {
NSLog(@"==removeLoadingHandle");
self.loading = NO;
[MBProgressHUD hideHUDForView:[UIApplication sharedApplication].keyWindow animated:YES];
}
}
- (void)addLoadingHandle
{
NSLog(@"addLoadingHandle");
if (!self.loading) {
NSLog(@"==addLoadingHandle");
self.loading = YES;
[MBProgressHUD showHUDWithMessage:nil];
}
}
掉單處理
其實(shí)只是添加了內(nèi)購(gòu)監(jiān)聽(tīng),若有未結(jié)束掉的事務(wù),則添加了內(nèi)購(gòu)監(jiān)聽(tīng)后,蘋(píng)果會(huì)將未結(jié)束掉的事務(wù)通過(guò)- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions回調(diào)到app,再進(jìn)行相應(yīng)處理
/**
掉單處理
*/
- (void)checkIAPHandle
{
[self addIAPObserverHandle];
}
在你認(rèn)為合適的地方添加內(nèi)購(gòu)監(jiān)聽(tīng),比如App啟動(dòng)時(shí),或者某些頁(yè)面,又或者你可以自行添加個(gè)定時(shí)器等進(jìn)行相應(yīng)的修改:
///掉單處理
[[IWRechargeTool sharedInstance] checkIAPHandle];
PS: 如果你們的內(nèi)購(gòu)流程需要app端將蘋(píng)果返回的購(gòu)買(mǎi)憑據(jù)和與其對(duì)應(yīng)的一個(gè)唯一值(比如orderId,接下來(lái)我暫且稱為orderId)傳輸給后臺(tái),建議和后臺(tái)重新設(shè)計(jì)一下內(nèi)購(gòu)流程。
因?yàn)槿绻切枰銓rderId和與其對(duì)應(yīng)的transaction傳給后臺(tái)的話,因?yàn)榭赡艽嬖诙鄠€(gè)掉單情況,那么你可能需要將所有你沒(méi)成功的orderId與其對(duì)應(yīng)的transaction都保存下來(lái),在某些時(shí)機(jī)嘗試將其再發(fā)送給后臺(tái);但這樣做 不僅前端的工作很累,并且不能夠處理所有的掉單,比如你FaceID/TouchID通過(guò)后,在監(jiān)聽(tīng)支付回調(diào)還未回調(diào)到app端時(shí),殺掉了app,此時(shí)雖然app進(jìn)程結(jié)束了,但用戶付款申請(qǐng)已經(jīng)發(fā)出,有可能用戶被扣款,但app端并未將該orderId對(duì)應(yīng)的transaction保存下來(lái),也未將此次購(gòu)買(mǎi)行為告訴自己后臺(tái),用戶購(gòu)買(mǎi)的商品也就不會(huì)到賬,因?yàn)槲幢4嬖搕ransaction你也無(wú)法對(duì)其發(fā)起重試。
IAP機(jī)制,只要你加了監(jiān)聽(tīng),檢測(cè)到有未結(jié)束的事務(wù),在- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions方法中就會(huì)將未結(jié)束掉的再次返回給你
設(shè)計(jì)的內(nèi)購(gòu)流程,應(yīng)該做到你只需要將憑據(jù)傳輸給自己服務(wù),后臺(tái)就可以通過(guò)驗(yàn)證得知該憑據(jù)是否有效以及用戶購(gòu)買(mǎi)的哪個(gè)商品(用戶信息是請(qǐng)求頭里通過(guò)token獲取的)