前言
說起蘋果支付锌唾,可能對于大多數(shù)的開發(fā)者寝殴,或者準(zhǔn)確來說大多數(shù)老板們比較頭痛的事情动遭。因為蘋果支付(即IAP仰猖,In-App Purchase捏肢,后文統(tǒng)稱IAP)意味著要分蘋果一杯羹,差不多30%的提成給蘋果饥侵。尤其是游戲充值鸵赫,這種動輒百萬的流水,怪不得蘋果的IAP抽成的主要營收都在游戲業(yè)務(wù)爆捞。
當(dāng)然奉瘤,這不是今天要說的重點勾拉。作為一位開發(fā)工程師煮甥,這種抽成問題不是我們討論的焦點盗温,但是如果老板們熱切期待你給出一個繞過IAP抽成的方案的時候,你可以參考我之前寫過的一篇記錄支付寶手機網(wǎng)站(WAP)支付踩過的坑成肘。
做過支付開發(fā)的童鞋都知道卖局,就客戶端開發(fā)來說,IAP不管是對比支付寶還是微信支付双霍,其集成難易程度都明顯高于這些第三方支付砚偶,第一感覺甚至覺得有些邏輯顯得臃腫和啰嗦,當(dāng)然洒闸,隨著我們一步一步揭開了IAP的神秘面紗染坯,就會慢慢明白,蘋果工程師的設(shè)計哲學(xué)及其用意丘逸。這篇文章我們就好好說說IAP那些事单鹿。
哪些商品可以使用IAP?
按照蘋果官方的說法深纲,如下:
You can use In-App Purchase to sell content, app functionality, and services.
總結(jié)來說仲锄,如下三點商品,必須走IAP湃鹊。
- 付費數(shù)字內(nèi)容儒喊。如電子雜志,小說币呵,音樂怀愧,游戲虛擬道具等。
- App付費功能余赢。如付費去除廣告掸驱、付費提供高級功能等。
- 付費虛擬服務(wù)没佑。
只要出售的產(chǎn)品不屬于上面三條毕贼,就可以不用IAP。
比如出售一些實物或者實體服務(wù)蛤奢。
當(dāng)然鬼癣,還是強烈建議遵守蘋果的規(guī)則來選型支付方案,因為蘋果對IAP的打壓非常嚴(yán)格啤贩,在近期的審核策略下待秃,兩次被同樣IAP問題拒審,就會被延審痹屹,再而就會有可能被封禁賬號章郁。
支持哪些IAP產(chǎn)品類型?
為了保證文章知識的完整性,這里稍微介紹一下IAP所支持的產(chǎn)品類型暖庄。當(dāng)然我們的重點是Coding聊替。
- 消耗型項目
只可使用一次的產(chǎn)品,使用之后即失效培廓,必須再次購買惹悄。
示例:釣魚 App 中的魚食。 - 非消耗型項目
只需購買一次肩钠,不會過期或隨著使用而減少的產(chǎn)品泣港。
示例:游戲 App 的賽道。 - 自動續(xù)期訂閱
允許用戶在固定時間段內(nèi)購買動態(tài)內(nèi)容的產(chǎn)品价匠。除非用戶選擇取消当纱,否則此類訂閱會自動續(xù)期。
示例:每月訂閱提供流媒體服務(wù)的 App踩窖。 - 非自動續(xù)期訂閱
允許用戶購買有時限性服務(wù)的產(chǎn)品惫东。此 App 內(nèi)購買項目的內(nèi)容可以是靜態(tài)的。此類訂閱不會自動續(xù)期毙石。
示例:為期一年的已歸檔文章目錄訂閱廉沮。
上面兩小節(jié)是系統(tǒng)介紹IAP,下面正式開始IAP的集成
集成IAP步驟一:填寫銀行卡和稅務(wù)信息
登錄 iTC后臺徐矩,進入 協(xié)議滞时、稅務(wù)和銀行業(yè)務(wù)
來進行系列初始化。
因為這小節(jié)不是重點滤灯,而且比較簡單坪稽,這里就不再贅述具體細(xì)節(jié)。簡單說下注意事項:
- 提前讓運營童鞋準(zhǔn)備好一張銀行卡鳞骤,將來IAP掙的錢蘋果就會打到這個卡上窒百。注意要知道行號和地址信息等。
- 報稅表只需填寫美國的就行豫尽。
集成IAP步驟二:創(chuàng)建付費商品
在 我的App > 功能 > App內(nèi)購買項目 欄目中篙梢,點擊+按鈕,根據(jù)具體情況添加商品美旧,大多數(shù)都是消耗型商品渤滞,如游戲幣等。
這小節(jié)同樣很簡單就不再贅述榴嗅。這里只說注意點:
- 產(chǎn)品ID要好好設(shè)計妄呕,不能重復(fù)且有意義,因為不管是后面的編程嗽测,還是服務(wù)端的產(chǎn)品配置酝润,都要這個ID。舉例:
com.tencent.mm.pay.coin_100
习柠。 - 注意價格不能自定義。要按照蘋果預(yù)設(shè)好的等級來添加商品停做,目前一共有87等級,最大支持
6498CNY
蠢护,一般來說足夠。
集成IAP步驟三:開始Coding
說起IAP的編程养涮,我先多說兩句葵硕。其實,看到過很多童鞋吐槽IAP的集成流程有些復(fù)雜且易出錯贯吓,的確是這樣懈凹,相比于AliPay、WePay悄谐,的確稍顯復(fù)雜介评,可能這也是移動支付沒競爭過天朝的原因吧哈哈。 不過玩笑歸玩笑爬舰,其實集成起來们陆,真的不難。
編碼之前情屹,要先知道支付流程的各個階段坪仇。否則就會難以下手±悖看下圖:
從上圖得知椅文,支付分三個階段:獲取支付信息、支付請求和交付商品惜颇。
我們分階段編程皆刺。
1、獲取商品信息
首先凌摄,我們從上文知道羡蛾,IAP商品是以產(chǎn)品ID做標(biāo)識的。我們展示給用戶商品之前锨亏,得需要對這些標(biāo)識做一些處理林说,來避免一些異常情況。具體如下:
- Product Identifiers(產(chǎn)品ID列表)
Product IDs 作為出售商品的唯一標(biāo)識屯伞,其重要性不言而喻腿箩。即上面步驟中在iTC添加商品的時候手動填寫的,所以要保證唯一和語義劣摇。
一般來說珠移,Product IDs 是放在自己的服務(wù)端進行動態(tài)獲取的,當(dāng)然,如果你足夠懶钧惧,也可以寫死在App本地暇韧。放在服務(wù)端的好處很多,比如防止過期的產(chǎn)品還在展出浓瞪,導(dǎo)致用戶購買無效產(chǎn)品懈玻。以后如果新增了產(chǎn)品,可以不用提包更新版本乾颁,就可以將最新產(chǎn)品展示給用戶涂乌。這點很實用,比如游戲經(jīng)常會推出新的道具商品等英岭,不需發(fā)版湾盒,即可完成新商品的添加。
不管是放在服務(wù)端诅妹,還是App本地罚勾,Product IDs 的序列化方式都是一樣的,即以數(shù)組方式存儲若干ID字符串吭狡。
[
"com.hoolai.bmt.level1",
"com.hoolai.bmt.level2",
]
- 檢測 Product IDs 的有效性
為了避免過期商品還在展示尖殃,導(dǎo)致用戶購買無效商品,我們需要對上一步獲取到的 IDs 進行驗證過濾划煮,將無效的產(chǎn)品進行置灰或者直接刪除分衫。代碼如下:
- (void)validateProductIDs:(NSArray <NSString *> *)ids {
_productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:ids]];
_productRequest.delegate = self;
[_productRequest start];
}
#pragma mark - <SKProductsRequestDelegate>
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
for (NSString *invalidID in response.invalidProductIdentifiers) {
// 處理無效的產(chǎn)品ID
}
[self displayStoreUI];
}
- (void)displayStoreUI {
}
2、請求支付
上一步我們獲取到了有效的 ProductIDs般此,當(dāng)用戶點擊了某個商品的購買按鈕時蚪战,如“購買10鉆石”,我們就需要發(fā)送支付請求給蘋果內(nèi)購系統(tǒng)铐懊。
// 該處的product是上面的協(xié)議方法didReceiveResponse返回的邀桑。
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.quantity = 1; //!< 購買該商品的數(shù)量
// 將payment添加到支付隊列
[[SKPaymentQueue defaultQueue] addPayment:payment];
但是,在向蘋果發(fā)送支付請求的時候科乎,有一個特別要注意的點壁畸,就是要做一下反欺詐引擎。為什么要這么做呢茅茂?
就買游戲幣為例捏萍,我們知道,正常情況下空闲,我們App的大多數(shù)不同的用戶會使用不同的AppleID賬號來購買游戲幣令杈。而如果發(fā)現(xiàn)一個同樣的用戶多次使用不同的AppleID賬號來購買游戲幣,這就不正常了碴倾,很可能這個用戶涉嫌支付欺詐逗噩。我們App之前就遭遇過類似情形掉丽,非法分子通過匯率不對稱,進行支付詭法來獲取差價异雁,擾亂了我們的價格定位和產(chǎn)品形象捶障,影響惡劣。
那纲刀,怎么解決這個問題呢项炼?
蘋果給出的建議是,在我們發(fā)送支付請求的時候示绊,需要告訴蘋果這個用戶的唯一性關(guān)聯(lián)的不透明標(biāo)識符锭部。有些繞口,通白來講耻台,就是想要這個用戶uid的單向哈希后的值空免】樟恚可以通過下面的哈希函數(shù)將用戶uid進行指紋計算盆耽,獲取到一個不可逆的hash值。
// Custom method to calculate the SHA-256 hash using Common Crypto
- (NSString *)hashedValueForAccountName:(NSString*)userAccountName {
const int HASH_SIZE = 32;
unsigned char hashedChars[HASH_SIZE];
const char *accountName = [userAccountName UTF8String];
size_t accountNameLen = strlen(accountName);
// Confirm that the length of the user name is small enough
// to be recast when calling the hash function.
if (accountNameLen > UINT32_MAX) {
NSLog(@"Account name too long to hash: %@", userAccountName);
return nil;
}
CC_SHA256(accountName, (CC_LONG)accountNameLen, hashedChars);
// Convert the array of bytes into a string showing its hex representation.
NSMutableString *userAccountHash = [[NSMutableString alloc] init];
for (int i = 0; i < HASH_SIZE; i++) {
// Add a dash every four bytes, for readability.
if (i != 0 && i%4 == 0) {
[userAccountHash appendString:@"-"];
}
[userAccountHash appendFormat:@"%02x", hashedChars[i]];
}
return userAccountHash;
}
獲取到hash值后扼菠,通過SKPayment
的applicationUsername
屬性告訴蘋果摄杂,即下行代碼,IAP內(nèi)部系統(tǒng)就會自動檢測反欺詐處理循榆。
payment.applicationUsername = [self hashedValueForAccountName:@"<UID>"];
3析恢、獲取交付回調(diào)
發(fā)送支付請求后,接下來IAP系統(tǒng)(即StoreKit框架)就會自動調(diào)起支付頁面秧饮,這時transaction queue
的起到了樞紐般的重要作用映挂,他是我們App與AppStore
進行支付溝通的橋梁。我們需要給transaction queue
綁定一個監(jiān)聽者盗尸,來獲取實時的狀態(tài)變化柑船,比如支付完成、支付失敗的狀態(tài)泼各。
這里要注意一點鞍时,注冊監(jiān)聽者的時間點要盡量提前。因為用戶的網(wǎng)絡(luò)環(huán)境是多變不穩(wěn)定的扣蜻,如果在用戶支付成功后逆巍,由于網(wǎng)絡(luò)問題或者崩潰Bug導(dǎo)致獲取購買的商品失敗,在應(yīng)用啟動的時候莽使,StoreKit系統(tǒng)
會再次通知transaction queue observer
锐极,從而進行自動補單,防止訂單丟失芳肌。
所以溪烤,在什么地方添加監(jiān)聽者呢味咳?蘋果建議在application:didFinishLaunchingWithOptions:
方法中添加。如下:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[SKPaymentQueue defaultQueue] addTransactionObserver:observer];
}
然后這個監(jiān)聽者需要遵守SKPaymentTransactionObserver
協(xié)議檬嘀,并實現(xiàn)下面方法:
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
// Call the appropriate custom method for the transaction state.
case SKPaymentTransactionStatePurchasing:
[self showTransactionAsInProgress:transaction deferred:NO];
break;
case SKPaymentTransactionStateDeferred:
[self showTransactionAsInProgress:transaction deferred:YES];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
break;
default:
// For debugging
NSLog(@"Unexpected transaction state %@", @(transaction.transactionState));
break;
}
}
}
在用戶輸入密碼槽驶,支付完成后,上面回調(diào)會被調(diào)用鸳兽,transactionState
為SKPaymentTransactionStatePurchased
掂铐,注意這時要了解一下支付憑證receipt
的概念,他是通過[[NSBundle mainBundle] appStoreReceiptURL]
獲取到揍异。
他是什么全陨?
從App Store安裝了一個應(yīng)用程序,它包含一個只有蘋果才能校驗的加密簽名后的receipt收據(jù)衷掷。 receipt存儲在應(yīng)用程序包中辱姨, 調(diào)用
NSBundle
類的appStoreReceiptURL
方法來查找收據(jù)。里面包含了用戶的購買信息記錄戚嗅,注意雨涛,里面有可能會包含多條購買記錄,因為購買信息記錄只會在finishTransaction
后才會從中刪除懦胞。他是一個二進制包替久,內(nèi)部結(jié)構(gòu)如下圖:
Receipt
他有什么作用?
- 先說一下最重要的作用躏尉,就是校驗用戶支付購買的可靠性蚯根。
當(dāng)用戶支付完成后,我們可以拿著receipt數(shù)據(jù)進行校驗用戶購買商品行為的合法性胀糜。這是一個重點颅拦,單獨放在下面第4小節(jié)講解。 - 防止軟件被重簽名復(fù)刻教藻,導(dǎo)致自己辛辛苦苦開發(fā)的付費軟件被非法分子破解距帅。
- 可以查詢用戶有沒有購買該件商品。方法如下:
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
BOOL rocketCarEnabled = [self receipt:receiptData includesProductID:@"com.tencent.mm.pay.coin_100"];
- 如果是非消耗型商品或者是自動續(xù)期的訂閱的商品怖竭,可以通過
SKReceiptRefreshRequest
來恢復(fù)購買锥债。
4、校驗購買憑證
我們知道痊臭,一旦涉及到支付行為哮肚,如果出現(xiàn)差錯,后果往往非常嚴(yán)重广匙。所以為了保證用戶購買行為的合法性允趟,蘋果使用的安全機制是receipt
校驗。
上面小節(jié)也提到了receipt
校驗鸦致,這種校驗本質(zhì)上是發(fā)送POST請求到蘋果服務(wù)器潮剪,(生產(chǎn)環(huán)境:https://buy.itunes.apple.com/verifyReceipt
涣楷,沙盒環(huán)境:https://sandbox.itunes.apple.com/verifyReceipt
),但是切記要通過自己的服務(wù)端去跟蘋果服務(wù)器進行校驗交互抗碰,因為如果使用App直接去跟蘋果服務(wù)器進行校驗無法避免中間人攻擊狮斗。
在用戶支付完成后,我們在transaction queue observer
的代理方法paymentQueue: updatedTransactions:
中可以獲取通過appStoreReceiptURL
獲取到含有剛剛支付信息的receipt
數(shù)據(jù)弧蝇。我們需要把它傳到自己的服務(wù)端碳褒,然后讓服務(wù)端去跟蘋果服務(wù)器交互校驗其合法性。
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
if (!receipt) {
// 沒有收據(jù)
return;
}
/* ... 發(fā)送給自己服務(wù)端 ... */
服務(wù)端拿到receipt
數(shù)據(jù)后看疗,訪問上面提到的蘋果服務(wù)器沙峻,參數(shù)receipt-data
為必傳,需要將其base64
編碼两芳。
這時候重點關(guān)注蘋果服務(wù)器的響應(yīng)體摔寨,核心在status
字段。
當(dāng)status=0
怖辆,標(biāo)志著校驗成功是复,用戶的購買為合法的。
當(dāng)status=21007
疗隶,說明這個receipt
是來自測試環(huán)境但是卻發(fā)送到了蘋果的生成環(huán)境校驗地址佑笋。自己服務(wù)端在收到這個狀態(tài)碼時只需要將域名定向到蘋果的生成環(huán)境校驗地址再進行訪問即可翼闹。這個很重要斑鼻,蘋果也建議除非是在測試階段,否則都優(yōu)先使用生產(chǎn)地址來校驗(如審核階段猎荠,online階段)坚弱。
后記
到這里,整個IAP的流程就走完了关摇,也許他確實不如支付寶荒叶、微信支付那樣簡單粗暴,但是這僅是針對客戶端同學(xué)來說的输虱。其實真實對比來看些楣,對于服務(wù)端同學(xué),拿支付寶來說宪睹,又要在支付寶后臺生成愁茁、上傳公鑰,又要設(shè)置安全域名亭病,又要進行簽名鹅很、生成預(yù)訂單、接收支付寶服務(wù)器回調(diào)罪帖、安全校驗等等系列工作促煮,你會發(fā)現(xiàn)其實IAP對服務(wù)端幾乎沒有工作量邮屁。換句話說,IAP只是因為將工作量轉(zhuǎn)移到了客戶端而顯得貌似復(fù)雜了而已菠齿。
但是佑吝,我們還是得要注意,為了保證IAP的穩(wěn)定性绳匀,即避免丟單迹蛤,我們還是得要做一些額外工作,簡單總結(jié)襟士,是如下幾小點:
- 使用
iCloud
或者App服務(wù)端保存receipt
等支付元信息防止用戶剛支付完成還沒來得及處理購買業(yè)務(wù)就把App卸載而造成的丟單(別笑盗飒,測試同學(xué)會這么干)。 - 將
transaction queue observer
的盡早前置陋桂,以保證蘋果IAP的回調(diào)能夠及時響應(yīng)處理而非進入到特定頁面才能收到回調(diào)逆趣。推薦放在didFinishLaunchingWithOptions
中執(zhí)行SKPaymentQueue.default().add(your_observer)
。 - 暫時就想到上面這些嗜历,等想到其他的再補充宣渗。
OK,先這樣梨州。