購買過程的最后一部分是應(yīng)用程序等待應(yīng)用商店處理支付請求,存儲本次購買的信息以便將來啟動(dòng),下載購買的內(nèi)容,然后標(biāo)記交易結(jié)束,如圖4-1。
一、等待應(yīng)用商店處理交易
交易隊(duì)列通過商店 Kit 框架在應(yīng)用和應(yīng)用商店的交流過程中起著核心作用。把應(yīng)用商店需要處理的工作添加到隊(duì)列,比如需要被處理的支付請求。 當(dāng)交易狀態(tài)改變時(shí)---比如,當(dāng)支付請求成功時(shí)---商店 Kit 調(diào)用應(yīng)用的交易隊(duì)列觀察者(observer)。 需要決定哪個(gè)類作為觀察者(observer)。 在很少的應(yīng)用中,可以在應(yīng)用委托中處理所有的商店 Kit 邏輯,包括觀察交易隊(duì)列。 在大多數(shù)應(yīng)用中,創(chuàng)建單獨(dú)的類來處理該觀察者邏輯和其余的應(yīng)用程序商店邏輯。 觀察者必須遵循 SKPaymentTransactionObserver 協(xié)議。
使用觀察者意味著應(yīng)用程序不會(huì)不斷地查詢其活動(dòng)交易的狀態(tài)。 除了使用交易隊(duì)列來處理支付請求,應(yīng)用程序還使用它來下載蘋果托管的內(nèi)容并找出已經(jīng)更新的訂閱。
當(dāng)應(yīng)用啟動(dòng)時(shí)注冊交易隊(duì)列,如列表4-1. 確保觀察者已經(jīng)隨時(shí)準(zhǔn)備好處理交易,而不只在添加一個(gè)交易到隊(duì)列后處理。 舉個(gè)例子,考慮用戶在進(jìn)入一個(gè)隧道(tunnel)之前正好在應(yīng)用中購買東西。 應(yīng)用不能傳遞被購的內(nèi)容,因?yàn)闆]有網(wǎng)絡(luò)連接。當(dāng)應(yīng)用下次啟動(dòng)時(shí),商店 Kit 再次調(diào)用交易隊(duì)列觀察者并在那時(shí)傳遞被購的內(nèi)容。 類似地,如果應(yīng)用程序處理交易失敗,每次應(yīng)用啟動(dòng)時(shí),商店 Kit 都會(huì)調(diào)用觀察者直到交易被正確地結(jié)束。
列表 4-1 注冊事務(wù)交易隊(duì)列觀察者
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
/* ... */
[[SKPaymentQueue defaultQueue] addTransactionObserver:observer];
}
在交易隊(duì)列觀察者中實(shí)現(xiàn) paymentQueue:updatedTransactions: 方法。 交易狀態(tài)改變時(shí),商店 Kit 調(diào)用該方法--- 比如,當(dāng)支付請求被處理時(shí)。 交易狀態(tài)告訴應(yīng)用該執(zhí)行什么動(dòng)作,如表 4-1 和列表 4-2. 隊(duì)列中的交易可以以任何順序改變狀態(tài)。 應(yīng)用需要準(zhǔn)備好在任何時(shí)候處理任何活動(dòng)交易。
表 4-1 交易狀態(tài)和相應(yīng)的動(dòng)作
列表 4-2 相應(yīng)交易狀態(tài)
-(void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
// Call the appropriate custom method.
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
default:
break;
}
}
}
為了在等待時(shí)保持用戶界面最新,交易隊(duì)列觀察者可以實(shí)現(xiàn)SKPaymentTransactionObserver 協(xié)議的以下可選方法:當(dāng)交易被從隊(duì)列中移除時(shí),調(diào)用paymentQueueRestoreCompletedTransactionsFinished: 方法--- 在該方法的實(shí)現(xiàn)中,從應(yīng)用的 UI 移除相應(yīng)的產(chǎn)品。 當(dāng)商店 Kit 結(jié)束恢復(fù)交易時(shí),根據(jù)是否有 error 發(fā)生調(diào)用paymentQueueRestoreCompletedTransactionsFinished: 或 paymentQueue:restoreCompletedTransactionsFailedWithError: 方法。 在這些方法的實(shí)現(xiàn)中,更新應(yīng)用的 UI 來反映成功或 error。
二、保留購買記錄
產(chǎn)品有效之后,應(yīng)用需要做購買的持久購買記錄。 當(dāng)啟動(dòng)時(shí),應(yīng)用使用該持久購買記錄讓產(chǎn)品變得有效。 它還使用該記錄來恢復(fù)購買,正如“Restoring Purchased Products.”中所述。 應(yīng)用的持久化策略取決于出售的產(chǎn)品類型以及 iOS 的版本。
- iOS 7 以及之后的版本,對于非消耗產(chǎn)品和自動(dòng)訂閱,使用應(yīng)用收據(jù)作為持久記錄。
- iOS 7 之前的版本,對于非消耗產(chǎn)品和自動(dòng)訂閱,使用用戶默認(rèn)系統(tǒng)或 iCloud 來保留持久記錄。
- 對于非自動(dòng)訂閱,使用 iCloud 或應(yīng)用服務(wù)器來保留持久記錄。
對于消耗產(chǎn)品,應(yīng)用更新它的內(nèi)部狀態(tài)來反映購買,但是沒有必要保留持久記錄因?yàn)楹牟漠a(chǎn)品不能恢復(fù)或不能跨設(shè)備同步。 確保被更新狀態(tài)是支持狀態(tài)保留(in iOS)對象的部分,或者是手動(dòng)保留整個(gè)應(yīng)用啟動(dòng)狀態(tài)的對象(in iOS 或者 OS X). 關(guān)于狀態(tài)保留的信息,請看iOS App Programming Guide 中的“State Preservation and Restoration” 。
當(dāng)使用用戶默認(rèn)系統(tǒng)(User Defaults system)或 iCloud 時(shí),應(yīng)用可以存儲值,可以是數(shù)字或布爾值,或者備份交易收據(jù)。 在 OS X 中,用戶可以使用 defaults 命令編輯用戶默認(rèn)系統(tǒng)。 存儲收據(jù)除了防止持久記錄被篡改外,還要求更多的應(yīng)用邏輯。
當(dāng)通過 iCloud 保留記錄時(shí),請注意應(yīng)用程序的持久記錄是跨設(shè)備同步的,但是在別的設(shè)備上有應(yīng)用負(fù)責(zé)下載任何相關(guān)內(nèi)容。
1、使用應(yīng)用收據(jù)來保留記錄
應(yīng)用記錄包括了用戶購買的記錄,它由蘋果公司加密簽名。更多詳情,請看 Receipt Validation Programming Guide.
關(guān)于消耗產(chǎn)品和無需更新訂閱的產(chǎn)品信息在它們被支付后加入收據(jù),并保留該信息直到結(jié)束這個(gè)交易。 當(dāng)結(jié)束交易后,該信息將被刪除,下次的收據(jù)被更新---比如,下次用戶新的購買。
所有其它類型的購買信息在它們被支付時(shí)加入收據(jù),并且收據(jù)被永久保留。
2.在用戶默認(rèn)系統(tǒng)或 iCloud 中保留數(shù)值
要想在用戶默認(rèn)系統(tǒng)或 iCloud 中保留信息,把該值設(shè)置為關(guān)鍵字(key)。
#if USE_ICLOUD_STORAGE
NSUbiquitousKeyValueStore *storage = [NSUbiquitousKeyValueStore defaultStore];
#else
NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
#endif
[storage setBool:YES forKey:@"enable_rocket_car"];
[storage setObject:@15 forKey:@"highest_unlocked_level"];
[storage synchronize];
#######A、在用戶默認(rèn)系統(tǒng)或 iCloud 中保留一個(gè)收據(jù)
要想在用戶默認(rèn)系統(tǒng)或 iCloud 中存儲一個(gè)交易收據(jù),把值設(shè)置為關(guān)鍵字(key)賦值給收據(jù)。
#if USE_ICLOUD_STORAGE
NSUbiquitousKeyValueStore *storage = [NSUbiquitousKeyValueStore defaultStore];
#else
NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
#endif
NSData *newReceipt = transaction.transactionReceipt;
NSArray *savedReceipts = [storage arrayForKey:@"receipts"];
if (!receipts) {
// Storing the first receipt
[storage setObject:@[newReceipt] forKey:@"receipts"];
} else {
// Adding another receipt
NSArray *updatedReceipts = [savedReceipts arrayByAddingObject:newReceipt];
[storage setObject:updatedReceipts forKey:@"receipts"];
}
[storage synchronize];
#######B、用自己的服務(wù)器保留
把收據(jù)的副本和某些憑據(jù)和識別碼發(fā)送到應(yīng)用的服務(wù)器,這樣可以隨時(shí)查看某個(gè)用戶的收據(jù)。 比如,讓用戶使用 email 或用戶名密碼登陸。不要使用 UIDevice 類的 identifierForVendor 特性---不能用它來認(rèn)證和恢復(fù)不同設(shè)備上同一個(gè)用戶的購買記錄,因?yàn)椴煌脑O(shè)備的該特性有不同的值。
三、解鎖應(yīng)用功能
如果產(chǎn)品設(shè)計(jì)開啟應(yīng)用功能,給開啟代碼設(shè)置布爾值并根據(jù)需要更新應(yīng)用界面。為了確認(rèn)解鎖什么功能,當(dāng)交易發(fā)生時(shí)咨詢應(yīng)用程序做的持久記錄。需要在購買完成以及應(yīng)用啟動(dòng)時(shí)更新該布爾值。
舉例子,使用應(yīng)用收據(jù),代碼應(yīng)該類似以下代碼:
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
// Custom method to work with receipts
BOOL rocketCarEnabled = [self receipt:receiptData
includesProductID:@"com.example.rocketCar"];
或者,使用用戶默認(rèn)系統(tǒng):
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL rocketCarEnabled = [defaults boolForKey:@"enable_rocket_car"];
然后使用該信息來開啟應(yīng)用程序中的相應(yīng)代碼路徑
if (rocketCarEnabled) {
// Use the rocket car.
} else {
// Use the regular car.
}
四、傳遞相關(guān)內(nèi)容
如果產(chǎn)品有相關(guān)內(nèi)容,應(yīng)用程序需要傳遞該內(nèi)容給用戶。 比如,購買游戲中的關(guān)卡需要傳遞定義了該關(guān)卡的文件。在音樂應(yīng)用中,購買額外的樂器需要傳遞那些樂器需要的聲音文件。
可以把這些內(nèi)容整合到應(yīng)用程序 bundle 中或者根據(jù)需要下載它--每種方法都有它的優(yōu)勢和劣勢。 如果應(yīng)用 bundle 中包含了太少的內(nèi)容,即使用戶購買再少的內(nèi)容也必須等待下載。 如果你的應(yīng)用 bundle 中包含了太多的內(nèi)容,應(yīng)用程序的初始下載太耗時(shí),對于那些不想購買相應(yīng)產(chǎn)品的用戶來說太浪費(fèi)內(nèi)存了。此外,若應(yīng)用程序太大,用戶將無法通過蜂窩網(wǎng)絡(luò)(cellular networks)下載它。
在應(yīng)用中嵌入少量的文件(最多幾兆),特別是如果期望大多數(shù)用戶可以購買該產(chǎn)品時(shí)。 應(yīng)用 bundle 中的內(nèi)容在用戶購買時(shí)可以立即提供。然而,要想添加或更新應(yīng)用 bundle 中的內(nèi)容,必須提交到蘋果商店應(yīng)用程序更新的版本。
需要時(shí)下載大量的文件。把內(nèi)容從應(yīng)用 bundle 中分離可以讓應(yīng)用在初次下載時(shí),應(yīng)用消耗時(shí)間少。比如,游戲可以在應(yīng)用 bundle 包含第一個(gè)關(guān)卡,并讓用戶在購買時(shí)下載剩余的關(guān)卡。 假設(shè)應(yīng)用程序從應(yīng)用服務(wù)器獲取它的產(chǎn)品識別碼列表,而不是硬性編碼在應(yīng)用 bundle 中,就不需要重復(fù)提交應(yīng)用程序來添加或更新應(yīng)用程序需要下載的內(nèi)容。
在 iOS 6 和以上版本中,大多數(shù)應(yīng)用程序都會(huì)使用蘋果托管的內(nèi)容作為下載文件。 在 Xcode 中的 In-App Purchase Content target(內(nèi)置購買內(nèi)容目標(biāo))來創(chuàng)建蘋果托管的內(nèi)容 Budle,并把它遞交到 iTunes Connect 中。當(dāng)把內(nèi)容托管到蘋果的服務(wù)器后,不需要在提供任何服務(wù)區(qū)---應(yīng)用內(nèi)容由蘋果來存儲,它使用相同的支持其他大型經(jīng)營相同的基礎(chǔ)設(shè)施(infrastructure),比如蘋果商店。 另外,蘋果托管的內(nèi)容即使應(yīng)用沒有在運(yùn)行也能自動(dòng)在后臺下載。
若有服務(wù)器基礎(chǔ)設(shè)施, 需要支持 iOS 老版本,或者是跨平臺共享服務(wù)器基礎(chǔ)設(shè)施,請選擇應(yīng)用服務(wù)器來托管內(nèi)容。
注意:不能修補(bǔ)應(yīng)用的二進(jìn)制或下載可執(zhí)行代碼。 當(dāng)遞交時(shí),應(yīng)用必須包含支持其所有功能所需的可執(zhí)行代碼。 如果新產(chǎn)品要求的代碼發(fā)生了改變,請遞交更新版本的應(yīng)用程序。
1.加載本地內(nèi)容
使用 NSBundle 類加載本地內(nèi)容,就像從應(yīng)用 Bundle 中加載其它資源一樣。
NSURL *url = [[NSBundle mainBundle] URLForResource:@"rocketCar"
withExtension:@"plist"];
[self loadVehicleAtURL:url];
2.從蘋果服務(wù)器下載托管內(nèi)容
當(dāng)用戶購買跟蘋果托管內(nèi)容相關(guān)的產(chǎn)品時(shí),交易被傳遞給交易隊(duì)列觀察者同時(shí)包含SKDownload 類對象,它讓下載相關(guān)的內(nèi)容。
要想下載內(nèi)容,通過調(diào)用SKPaymentQueue類的SKPaymentQueue: 方法,從交易的download特性中把下載對象添加到交易隊(duì)列。如果 downloads 屬性的值為 nil, 就表示該交易沒有蘋果托管內(nèi)容。 不像下載應(yīng)用程序,當(dāng)內(nèi)容超出一個(gè)特定大小時(shí),下載內(nèi)容不會(huì)自動(dòng)請求一個(gè) Wi-Fi 連接。如果沒有用戶的明確操作避免使用蜂窩網(wǎng)絡(luò)來下載大文件。
在交易隊(duì)列觀察者里實(shí)現(xiàn)paymentQueue:updatedDownloads: 方法來響應(yīng)下載狀態(tài)的改變---比如,通過在 UI 里更新進(jìn)程。 如果下載失敗,把 error 特性設(shè)置為該失敗信息呈現(xiàn)給用戶。
確保應(yīng)用程序能優(yōu)雅地處理 errors。比如,如果設(shè)備下載時(shí)磁盤空間不足,讓用戶選擇丟棄本次下載或者在稍后當(dāng)空間充足時(shí)再次恢復(fù)下載。
當(dāng)使用 progress 和 timeRemaining 屬性的值進(jìn)行下載時(shí),更新應(yīng)用用戶界面。可以在 UI 中使用 SKPaymentQueue 類的pauseDownloads:, resumeDownloads:, 和 cancelDownloads: 方法來讓用戶控制下載。 使用 downloadState特性來確定下載是否完成。 不要使用 download 對象的 progress 或 timeRemaining屬性來檢查它的狀態(tài)---這些狀態(tài)用來更新應(yīng)用 UI。
注意:在交易結(jié)束前下載所有的蘋果托管內(nèi)容。 交易完成后,它的下載對象將不能再被使用。
在 iOS 中,應(yīng)用程序可以管理下載的文件。 文件通過商店 Kit 框架被存儲在 Caches 文件夾中,它們都沒有設(shè)置備份標(biāo)記。 下載完成之后,應(yīng)用程序負(fù)責(zé)把它們移動(dòng)到恰當(dāng)?shù)奈恢谩?對于那些可以被刪除的內(nèi)容,比如設(shè)備內(nèi)存不足(并且稍后會(huì)由應(yīng)用程序重新下載)的內(nèi)容,則被留在 Caches 文件夾中。否則,把文件移動(dòng)到 Documents 文件夾并給它們設(shè)置標(biāo)記以防止它們從用戶的備份中丟失。
列表 4-3 Excluding downloaded content from backups
NSError *error;
BOOL success = [URL setResourceValue:[NSNumber numberWithBool:YES]
forKey:NSURLIsExcludedFromBackupKey
error:&error];
if (!success) { /* Handle error... */ }
在 OS X, 下載的文件由系統(tǒng)管理;應(yīng)用不能直接移動(dòng)或刪除它們。 要想在下載完成后定位這些內(nèi)容,使用 download 對象的contentURL 屬性。 要想在后續(xù)啟動(dòng)中定位這些文件,使用 SKDownload類的 contentURLForProductID: 類方法。要想刪除文件,使用 deleteContentForProductID:類方法。 關(guān)于從應(yīng)用收據(jù)讀取產(chǎn)品識別碼的更多信息,請看 Receipt Validation Programming Guide.
3.從應(yīng)用服務(wù)器中下載內(nèi)容
正如應(yīng)用和服務(wù)器之間的所有其它交互一樣,處理從應(yīng)用的服務(wù)器下載內(nèi)容的細(xì)節(jié)和過程機(jī)制都是開發(fā)者責(zé)任。該通信至少包括以下步驟:
- 應(yīng)用給服務(wù)器發(fā)送收據(jù)并請求內(nèi)容。
- 服務(wù)器驗(yàn)證收據(jù)來證明(establish)內(nèi)容已經(jīng)被購買,正如Receipt Validation Programming Guide中所述。
- 假設(shè)收據(jù)有效,服務(wù)器給應(yīng)用程序提供內(nèi)容。請確保應(yīng)用能優(yōu)雅地處理 errors。 比如,如果設(shè)備在下載時(shí)空間不足,讓用戶選擇時(shí)丟棄已經(jīng)下載的內(nèi)容或者在稍后等空間充足時(shí)再次恢復(fù)下載。考慮如何托管內(nèi)容和應(yīng)用如何跟服務(wù)器通信的安全隱患。更多信息,請看Security Overview.
五、結(jié)束交易
結(jié)束交易就是告訴商店 Kit 已經(jīng)完成購買所需內(nèi)容。 沒有結(jié)束的交易一直保留在隊(duì)列中直到它們結(jié)束,并且應(yīng)用程序每次啟動(dòng)時(shí)調(diào)用交易隊(duì)列觀察者,這樣應(yīng)用就可以結(jié)束交易。 應(yīng)用需要結(jié)束每筆交易,不管交易成功與否。
在結(jié)束交易之前完成所有以下操作:
- 保存購買記錄。
- 下載相關(guān)內(nèi)容。
- 更新應(yīng)用程序的 UI 來讓用戶訪問產(chǎn)品。
- 要想結(jié)束交易,在支付隊(duì)列中調(diào)用 finishTransaction: 方法。
SKPaymentTransaction *transaction = <# The current payment #>;
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
結(jié)束了交易后,不要對那個(gè)交易做任何操作或者不要再做任何工作來傳遞產(chǎn)品。 如果有任何工作沒完成,則表示應(yīng)用程序還沒準(zhǔn)備好結(jié)束該交易。
注意:不要在交易真正完成之前,嘗試調(diào)用 finishTransaction: 方法,在應(yīng)用中嘗試使用一些其它機(jī)制來跟蹤未結(jié)束交易。商店 Kit 不是這么用的。這么做的話會(huì)阻止下載蘋果托管內(nèi)容并可能導(dǎo)致其它問題。
六、建議測試步驟
測試代碼的每個(gè)部分來認(rèn)證已經(jīng)正確地實(shí)現(xiàn)內(nèi)購。
1、 測試一個(gè)支付請求
創(chuàng)建一個(gè)SKPayment實(shí)例使用有效的產(chǎn)品標(biāo)識符來測試。設(shè)置一個(gè)斷點(diǎn)來檢查(inspect)支付請求。 把支付請求添加到交易隊(duì)列,并設(shè)置一個(gè)斷點(diǎn)來確認(rèn)(comfirm)觀察者已經(jīng)調(diào)用了 paymentQueue:updatedTransactions: 方法。
測試過程中,可以立即結(jié)束交易而不需要提供內(nèi)容。 然而,即使是在測試過程中,結(jié)束交易失敗也可能導(dǎo)致問題:未結(jié)束的交易將一直留在隊(duì)列中,它可能影響以后的測試。
2、認(rèn)證交易觀察者代碼
檢查交易觀察者的 SKPaymentTransactionObserver 協(xié)議的實(shí)現(xiàn)。 認(rèn)證它可以處理交易,即使目前沒有顯示你的應(yīng)用程序的商店 UI,即使沒有在近期沒有購買。
在代碼中定位 SKPaymentQueue 類的 addTransactionObserver:方法的調(diào)用。 認(rèn)證應(yīng)用程序在應(yīng)用啟動(dòng)時(shí)調(diào)用了該方法。
3、測試成功地交易
用測試用戶賬號登陸應(yīng)用商店,在應(yīng)用中做購買。 在交易隊(duì)列觀察者的 paymentQueue:updatedTransactions: 方法實(shí)現(xiàn)中設(shè)置一個(gè)斷點(diǎn),并檢查交易來認(rèn)證它的狀態(tài)是 SKPaymentTransactionStatePurchased.
在保留購買記錄代碼中設(shè)置斷點(diǎn),并確保該代碼在響應(yīng)成功地購買時(shí)調(diào)用。 檢查用戶默認(rèn)系統(tǒng)或者 iCloud 鍵值存儲,并確認(rèn)已經(jīng)記錄了正確的信息。
4、測試中斷的交易
在交易隊(duì)列觀察者的 paymentQueue:updatedTransactions: 方法中設(shè)置一個(gè)斷點(diǎn),就可以控制它是否傳遞了產(chǎn)品。 然后在測試環(huán)境中像平時(shí)一樣購買,用斷點(diǎn)來暫時(shí)忽視該交易----比如,通過使用 LLDB 中的 thread return 命令從方法內(nèi)立即返回。
終止和重新啟動(dòng)應(yīng)用。商店 Kit 在啟動(dòng)后不久再次調(diào)用 paymentQueue:updatedTransactions: 方法;這次,讓應(yīng)用程序正常的響應(yīng)。認(rèn)證應(yīng)用正確地傳遞了產(chǎn)品并完成交易。
5、認(rèn)證交易已經(jīng)結(jié)束
定位應(yīng)用程序在哪調(diào)用了 finishTransaction: 方法。認(rèn)證所有跟交易相關(guān)的工作都已經(jīng)在該方法調(diào)用之前完成,該方法在每個(gè)交易中都調(diào)用,不管交易成功與否。