關(guān)于蘋果支付IAP的那些事

前言

說起蘋果支付锌唾,可能對于大多數(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湃鹊。

  1. 付費數(shù)字內(nèi)容儒喊。如電子雜志,小說币呵,音樂怀愧,游戲虛擬道具等。
  2. App付費功能余赢。如付費去除廣告掸驱、付費提供高級功能等。
  3. 付費虛擬服務(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ù)雜介评,可能這也是移動支付沒競爭過天朝的原因吧哈哈。 不過玩笑歸玩笑爬舰,其實集成起來们陆,真的不難。
編碼之前情屹,要先知道支付流程的各個階段坪仇。否則就會難以下手±悖看下圖:


image.png

從上圖得知椅文,支付分三個階段:獲取支付信息、支付請求和交付商品惜颇。
我們分階段編程皆刺。

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值后扼菠,通過SKPaymentapplicationUsername屬性告訴蘋果摄杂,即下行代碼,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)用鸳兽,transactionStateSKPaymentTransactionStatePurchased掂铐,注意這時要了解一下支付憑證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é)襟士,是如下幾小點:

  1. 使用iCloud或者App服務(wù)端保存receipt等支付元信息防止用戶剛支付完成還沒來得及處理購買業(yè)務(wù)就把App卸載而造成的丟單(別笑盗飒,測試同學(xué)會這么干)。
  2. transaction queue observer的盡早前置陋桂,以保證蘋果IAP的回調(diào)能夠及時響應(yīng)處理而非進入到特定頁面才能收到回調(diào)逆趣。推薦放在didFinishLaunchingWithOptions中執(zhí)行SKPaymentQueue.default().add(your_observer)
  3. 暫時就想到上面這些嗜历,等想到其他的再補充宣渗。

OK,先這樣梨州。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末痕囱,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子暴匠,更是在濱河造成了極大的恐慌鞍恢,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件每窖,死亡現(xiàn)場離奇詭異帮掉,居然都是意外死亡,警方通過查閱死者的電腦和手機窒典,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門蟆炊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瀑志,你說我怎么就攤上這事涩搓。” “怎么了劈猪?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵昧甘,是天一觀的道長。 經(jīng)常有香客問我岸霹,道長疾层,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任贡避,我火速辦了婚禮痛黎,結(jié)果婚禮上予弧,老公的妹妹穿的比我還像新娘。我一直安慰自己湖饱,他們只是感情好掖蛤,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著井厌,像睡著了一般蚓庭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仅仆,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天器赞,我揣著相機與錄音,去河邊找鬼墓拜。 笑死港柜,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的咳榜。 我是一名探鬼主播夏醉,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼涌韩!你這毒婦竟也來了畔柔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤臣樱,失蹤者是張志新(化名)和其女友劉穎靶擦,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體擎淤,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡奢啥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年秸仙,在試婚紗的時候發(fā)現(xiàn)自己被綠了嘴拢。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡寂纪,死狀恐怖席吴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捞蛋,我是刑警寧澤孝冒,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站拟杉,受9級特大地震影響庄涡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜搬设,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一穴店、第九天 我趴在偏房一處隱蔽的房頂上張望撕捍。 院中可真熱鬧,春花似錦泣洞、人聲如沸忧风。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽狮腿。三九已至,卻和暖如春呕诉,著一層夾襖步出監(jiān)牢的瞬間缘厢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工甩挫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留昧绣,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓捶闸,卻偏偏與公主長得像夜畴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子删壮,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容