你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 自動更新訂閱IAP淺談(設置和測試)

自動更新訂閱IAP淺談(設置和測試)

編輯:IOS開發基礎

1-j6o_QkSqzD0yvoJmEkYgeQ.png

本文由CocoaChina譯者Leon(社區ID)翻譯
作者:Jaz Garewal
原文:How to Set Up and Test an Auto-Renewable Subscription for an iOS App
轉載請保留原文內容和所有鏈接。


自動更新的訂閱是iOS內購形式的一種。它可以讓app在一個時間段內提供內容或功能。我在之前的帖子中聊過自動更新訂閱類型的IAP的准則,還聊過Apple以後可能做出的改進。現在讓我們來看一下如何實際地在App中設置並測試自動更新的訂閱。

在iTunes Connect後台設置自動更新的訂閱

事實上,實現自動更新的訂閱的流程和其他IAP並沒有什麼不同。有許多關於其他類型IAP的教程,比如:這一篇,主要講的是非更新的IAP類型(你可以直接跳到教程中段,講"添加訂閱到你的產品列表中")。設置自動更新訂閱和非自動更新訂閱的不同之處在於:自動更新訂閱的每個訂閱周期都在一個IAP實體中設置,而非自動更新訂閱的每個訂閱周期都需要單獨創建一個產品(比如3個月訂閱、6個月訂閱等),每個周期都有針對的一個product ID。

在App中實現自動更新的內購項目

我們發現,使用IAP helper類來實現自動更新訂閱式內購是最好的選擇,這和實現其他類型的內購,在策略上並無不同。但是,自動更新訂閱式內購對收據(receipt)管理有著額外的要求,我們可以再創建特定的helper類,來分別處理這方面的邏輯。這樣,helper類就可以處理各種各樣的內購項目,而不局限於自動更新式訂閱內購。在該教程中,簡潔起見,我們把所有的方法都放在同樣的一個IAP helper類中。而在實際操作中,我們建議把收據管理的代碼放在單獨的類中。考慮到收據交換的過程中,會涉及到私鑰,也可能將這部分的邏輯放到server處理。稍後我們會具體講解。

對於IAPHelper類來說,我們需要import StoreKit的framework,並聲明其遵循SKProductsRequestDelegate和SKPaymentTransactionObserver協議:

import UIKit
import StoreKit
class IAPHelper: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver

然後聲明初始屬性(後面我們再添加其他屬性):

var productID:NSSet = NSSet(object: "string")
var productsRequest:SKProductsRequest = SKProductsRequest()
var products = String : SKProduct

在App中,IAPHelper需要成為一個單例。在Swift 1.2中,有個簡潔的做法,比Objective-C中的dispatch_once方式更簡單。

static let sharedInstance = IAPHelper()

很簡單吧,Objective-C中的單例實現相當繁瑣,而使用Swift,單例簡潔到超乎想象。

現在屬性都准備好了,讓我們看看方法的實現。

首先,我們需要從App Store上請求產品的標識,然後啟動產品請求。因此,需要如下的public方法:

func requestProductsWithProductIdentifiers(productIdentifiers: NSSet) {

let productIdentifiers:NSSet = NSSet(objects: "com.ourApp.monthly", "com.ourApp.annually")

productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as Set)
productsRequest.delegate = self;
productsRequest.start()

}

設置了SKProductsRequest的delegate後,還需要一個方法從respose中提取信息,實現以下的delegate方法:

func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) {

    if let productsFromResponse = response.products as? [SKProduct] {
    
        for product in productsFromResponse {
        products[product.productIdentifier] = product
        }
    }
 }

現在,准備工作已經做好了,需要通過IAPHelper的shareInstance,調用requestProducts方法。

首先,我們需要添加一個新的屬性--一個閉包。通過它傳遞購買的狀態,根據狀態,再進行特別處理。閉包有一個Bool參數(true表示成功,false表示失敗),還有可選的String類型參數,在購買失敗時,可以傳遞一些錯誤信息。

var purchaseCompleted: ((Bool, String ) -> Void)

購買產品的第一步,要通過一個方法獲取購買的請求,比如點擊一個按鈕,來啟動訂閱的購買。

func beginPurchaseFor(productIDString: String, purchaseSucceeded:(Bool, String?) -> Void) {

    if SKPaymentQueue.canMakePayments() {
    
        if let product: SKProduct = products[productIDString] {
        
            purchaseCompleted = purchaseSucceeded
            var payment = SKPayment(product: product)
            SKPaymentQueue.defaultQueue().addTransactionObserver(self)
            SKPaymentQueue.defaultQueue().addPayment(payment);
        } else {
            purchaseSucceeded(false, "Product \(productIDString) not found")
        }
     } else {
        purchaseSucceeded(false, "User cannot make payments")
    }
 }

通過第一行的方法簽名可以看到,第二個參數是作為回調方法來使用的,它和我們的purchaseCompleted屬性一樣。

方法中,我們先驗證該設備是否可以進行IAP購買,如果可以,進一步驗證是不是有一個productID和傳入的匹配。若驗證都通過,就設置purchaseSucceeded參數到purchaseCompleted屬性,這樣就可以將狀態通過purchaseCompleted屬性進行回傳。接下來,我們創建一個SKPayment對象,並將self(我們的IAPHelper實例)設置為交易觀察者(transaction observer),然後將payment添加到default的支付隊列(payment queue)中。

上述過程中,如果有一項檢查失敗(如設備不允許內購或者product ID不匹配),我們都可以通過purchaseSucceeded回調傳遞false的狀態,並附上錯誤的詳細信息(如"產品(代碼xxx)不存在"或者"用戶不允許進行內購買")。通過這些,再判斷下一步的處理。

假定產品購買成功,得通過一個方法來監聽購買的響應事件。在這裡,需要實現SKPaymentTransactionObserver協議的paymentQueue方法。但是這裡我把詳細說明分成兩部分,首先,看看成功購買的處理:

func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
    for transaction: AnyObject in transactions {
    
        if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction {
        
            switch trans.transactionState {
                case .Purchased:
                SKPaymentQueue.defaultQueue().finishTransaction(trans)
                if let receiptURL = NSBundle.mainBundle().appStoreReceiptURL where
                NSFileManager.defaultManager().fileExistsAtPath(receiptURL.path!) 
{
                    if let purchaseSuccessCallback = purchaseCompleted {
                        purchaseSuccessCallback(true, nil)
                    }
                    
               self.isPurchasing = false
               self.receiptValidation()
        } else {
        
            if !isRefreshingReceipt {
            
                self.isPurchasing = false
                isRefreshingReceipt = true
                let request = SKReceiptRefreshRequest(receiptProperties: nil)
                request.delegate = self
                request.start()
                
                if let _ = purchaseCompleted {
                
                    purchaseCompleted**(true, nil)
                }
            }
        }
            break

假定購買過程順利,我們高興地得到了"已購買"狀態,不用再去糾結標准IAP實現的細節。在上面的代碼中你可以看到,我們需要那個收據信息。交易成功後,app可以通過appStoreReceiptURL得到用戶的本地收據地址,如果可以得到收據的話,我們就可以繼續進行下一步receiptValidation,這是設置、管理用戶訂閱的前提。在這個時間點,我們通過purchaseSuccessCallBack屬性回調的信息有:bool類型的true值,表明購買已成功;nil的error message。這些好消息將被發送給IAPHelper的使用者,供其後期處理。

如果,萬一,購買成功,但是還是得不到收據信息,我們可以通過新建SKReceiptRefreshRequest的方式來在後面踢一腳。

這裡我們先跳過paymentQueue,看一下SKReceiptRefreshRequest的requestDidFinish delegate方法。

 if let appStoreReceiptURL = NSBundle.mainBundle().appStoreReceiptURL where
    NSFileManager.defaultManager().fileExistsAtPath(appStoreReceiptURL.path!) {
    
        if 
NSFileManager.defaultManager().fileExistsAtPath(appStoreReceiptURL.path!) {

            self.receiptValidation()
            
            if let purchaseSuccessCallback = purchaseCompleted {
            
                purchaseSuccessCallback(true, nil)
                
            }
         }
    } else {
        if let purchaseSuccessCallback = purchaseCompleted {
            purchaseSuccessCallback(false, "Cannot find receipt")
        }
    }
}

當在requestDidFinish方法中獲得響應時,我們可以檢查appStoreReceiptURL路徑上是否有相關文件存在。若存在,我們通知那些正在等待回調的對象,告知它們購買已經成功,同時,可以繼續進行收據的有效性驗證。

如果這時還是無法找到收據,接著還是得返回錯誤的處理方法,傳遞false的Bool值,還有"無法找到收據"的錯誤信息--我們的IAPHelper在哭訴。有點像虛擬的收銀機被卡住了,無法打印。這樣,相關的對象可以處理這些IAPHelper收集的錯誤。

哦,這個話題太沉重了,我們來看點高興的。

接下來,看看如果我們的paymentQueue相關的delegate方法受到"Failed"狀態,該如何處理。什麼?不是說高興的事麼?我感覺世界都變得陰暗起來了。

不管怎樣,讓我們繼續完成我們的func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!)方法吧:

       case .Failed:
       
        self.isPurchasing = false
        
            if let _ = purchaseCompleted {
            
                var errorMessage: String?
                
                    if let _ = transaction.error {
                    
                        errorMessage = transaction.error.localizedDescription
                        
                     }
                     
                 purchaseCompleted(false, errorMessage)
                 
            }
            
        SKPaymentQueue.defaultQueue().finishTransaction(trans)
        SKPaymentQueue.defaultQueue().removeTransactionObserver(self)
        break
        
       default:
       break
       
           }
       }
    }
}

這麼說,方法失敗了?這不是Apple的API嗎?它運轉正常,順風順水,但還是失敗了。一定是打開方式不對。。。接下來,退回去,看著鏡子,想想該對Apple大神做個什麼表情。手腕上的Apple Watch已經不見了,你知道是怎麼回事。

生氣歸生氣,還是得看看錯誤處理。收到錯誤後,先通過localizedDescription拿到本地化的錯誤信息。將false和這個錯誤信息都回調回去(滿滿的都是淚)。接下來告訴SKPaymentQueue.defaultQueue()支付戰役已經結束,我們輸了。

以上內容將貫穿整個自動更IAP的實現過程。下面我們來看看為何我們需要收據信息,並學習如何進行處理。

自動更新IAP的訂閱管理

在深入研究代碼之前,讓我們先來理解一下自動更新IAP中訂閱管理的工作原理,看看它對App的意義在何處。

Apple並沒有在iOS系統中提供任何關於訂閱詳情的API或REST接口,也沒有提供可以查看訂閱更新或者取消的回調方法。它只提供了一個API:當你傳遞用戶本地的收據和一個"shared secret"(共享秘鑰,在iTunes Connect後台創建)時,返回一個包含用戶購買歷史的JSON數據,它裡面就包含了訂閱的詳細信息。

我們需要通過某種方式來持續檢測訂閱,通過周期性的獲取這個JSON數據,解析並查看其中的過期時間是否改變,以此來檢測訂閱是否更新或者取消。如果過期時間已經改變,則訂閱被更新;如果過期時間未改變,且當前已經超過了過期時間,則可視為訂閱未被更新。這種情況下,可以假定訂閱已經被取消(稍後可以通過解析返回數據,獲得取消日期--cancellation date,來做進一步判定),也可以認為從Apple返回的支付信息出現錯誤。不管是哪種情況,都可以認為當前的訂閱是無效的,對於這個用戶來說,App中的訂閱內容是不可用的。

以上邏輯可以在本地處理,也可以自己設置服務器,或者使用公開的後端服務,比如Parse。

說的夠多了,上代碼!

錯了,現在還不到寫代碼的時候,首先,需要在iTunes Connect後台創建shared secret。先跳轉到app的IAP設置部分,在IAP列表下面,你會看到"View or generate a shared secret",點擊即可:

blob.png

好了,現在可以看看收據處理的代碼了。

第一個方法是:

    if let receiptPath = NSBundle.mainBundle().appStoreReceiptURL?.path where
    NSFileManager.defaultManager().fileExistsAtPath(receiptPath), let receiptData 
= NSData(contentsOfURL: 
    NSBundle.mainBundle().appStoreReceiptURL!), let receiptDictionary = ["receipt-
data" : 
    receiptData.base64EncodedStringWithOptions(nil), "password" : "your shared 
secret"], let requestData = 
    NSJSONSerialization.dataWithJSONObject(receiptDictionary, options: nil, 
error:&error) as NSData! {

        let storeURL = NSURL(string: 
"https://sandbox.itunes.apple.com/verifyReceipt")!
        var storeRequest = NSMutableURLRequest(URL: storeURL)
        storeRequest.HTTPMethod = "POST"
        storeRequest.HTTPBody = requestData
        
        let session = NSURLSession(configuration: 
NSURLSessionConfiguration.defaultSessionConfiguration())

        session.dataTaskWithRequest(storeRequest, completionHandler: { (data: 
NSData!, response: NSURLResponse!,
        connection: NSError!) → Void in
        
           if let jsonResponse: NSDictionary = 
NSJSONSerialization.JSONObjectWithData(data, options:
           NSJSONReadingOptions.MutableContainers, error: &error) as? 
NSDictionary, let expirationDate: NSDate = 
           self.expirationDateFromResponse(jsonResponse) {
           
               self.updateIAPExpirationDate(expirationDate)
               
           }
       })
    }
}

方法中聲明了optional類型的NSError變量。接下來是溫習Swift 1.2中optional類型的chaining capabilities的好機會。按照鏈式的結構,先判斷是否存在appStoreReceiptURL(在購買完成時,我們已經驗證過了這個屬性,但是Swift中,多一層檢查,就多一重保障)。接下來,檢查這個path下是否有文件存在,如果存在的話,就通過該文件創建receiptData對象。然後,再填上shared secret,就創建了一個receiptDictionary JSON數據。

上述過程完成後,就可以用requestData創建NSMutableRequest。

在本例子中,我們通過沙箱URL來驗證收據,在實際的環境中,你需要把"sandbox"換成"buy"。

我們將requestData數據發送給Apple,等待它對completionHandler的回調。回調收到後,繼續通過optional chain方式檢查返回數據:如果第一部分(這裡就是data轉到JSON)存在的話,就用expirationDateFromResponse方法在裡面找到expiration date數據。

進展順利的話,我們就可以在適當的時機處理該過期時間--檢查該時間和我們之前存儲的是否一致(如果一致,表明訂閱已更新),若過期時間等於或者早於當前日期,則意味著,訂閱未更新,很可能已經被取消,可以通過JSON數據中的Cancellation Date進一步檢查。

現在我們來創建expirationDateFromResponse,從JSON返回數據中解析過期時間:

func expirationDateFromResponse(jsonResponse: NSDictionary) → NSDate? {

   if let receiptInfo: NSArray = jsonResponse["latest_receipt_info"] as? NSArray {
   
      let lastReceipt = receiptInfo.lastObject as! NSDictionary
      var formatter = NSDateFormatter()
      formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV"
      
      let expirationDate: NSDate = 
formatter.dateFromString(lastReceipt["expires_date"] as! String) as NSDate!

      return expirationDate
   } else {
      return nil
   }
}

這個方法中,先看看"latestreceiptinfo"的key是否對應NSArray類型的數據。如果有的話,我們從數組中獲取最後一個對象,再從這個對象中查找key"expires_date"對應的對象,將其轉換成NSDate類型。上述過程無誤的話,就可以返回expirationDate對象。過程中出錯的話,就直接返回nil。

接下來,我們來學習如何測試自動更新訂閱IAP。

測試自動更新類IAP

完成收據驗證代碼只是第一步,問題是如何驗證我們的驗證代碼?答案是:測試。你的代碼依賴於外部的模塊(IAP服務器),而你無法控制,只能通過一些工具來進行測試。

之前提到過,有兩個URL可以用來驗證收據,一個針對測試環境,另外一個針對實際環境。沙箱模式下購買自動更新訂閱類IAP和測試其他類型的IAP過程類似。只需要在iTunes Connect後台創建沙箱測試賬戶(Users and Roles功能下),可以創建多個測試賬戶,以備測試之需。然後就可以用沙箱賬戶在設備上完成內購。

測試自動更新類IAP時,有一個不同之處:該購買是有周期的。訂閱會於5次更新後作廢。看到這裡你肯定會想:"等等,要是我設置了每月的訂閱,要測試過期得等到5個月後?"實際上,自動更新的IAP在沙箱環境下,周期是會加速的,更新是按分鐘或小時計算。如果你要測試一月的訂閱,更新周期是5分鐘。因此要測試過期(5次更新後),等待25分鐘即可。

blob.png

另外,需要注意的一點是:在測試環境下,訂閱並不是每次都自動更新。我估算大概有三分之一的概率,月度訂閱會在5分鐘後被標記一次,然後,就直接過期,並不會被自動更新。記得我提醒過你多創建幾個測試賬戶嗎?那就是為了方便在測試訂閱更新時使用。要是你需要測試過期處理的代碼,就一直用過期的那個賬戶,這樣就可以將等待時間減少到原來的五分之一。

也就是說,在測試一周自動更新訂閱時,每次都等15分鐘並不明智。另外,如何測試訂閱取消?壞消息是,你無法測試。當前,測試環境的訂閱無法手動進行取消。因此,最好的方式是:創建多個沙箱測試賬號,用一個先測試,再用其他測試。等第一個快要過期時,切換過去測試。

結論備注

學到這裡,你不僅知道了如何實現自動更新類IAP,還知道了如何進行測試。該教程有一點沒有講到:如何將收據驗證碼,和IAP的shared secret key保存到服務器。這是我們後面需要研究的。現在,希望現有知識可以幫助你順利實現訂閱功能。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved