MR SHIH

必幸施

In-App-Purchase交易模組設計

| Comments

網路上很多介紹如何運用StoreKit裡面的API在iOS上付款的文章,但實務上因為In-App-Purchase是程式裡面需要密集配合業務需求的部分,如果沒有一個良好抽象化的設計,在高可維護性與彈性,可擴展能力下功夫,一旦業務需求一複雜或反覆迭代更改,就會讓IAP相關的邏輯變得難以修改與維護。

以上原因,所以如何在APP中設計一套可維護可擴展易整合的IAP架構,是開發大型APP與進階開發者應該關心的議題。接下來的文章把IAP付款架構有關執行交易與交易結果處理的這部分,抽象成TNStoreObserver類別。

StoreObserver

付款流程是APP根據一組定義在iTunesConnect的商品ID,向Apple Server請求對應的SKPayment物件,裡面包含了Localization的商品名稱與價錢,把這個想成一個商品。而一旦把這個SKPayment物件放到由系統維護的SKPaymentQueue時,這時候就開始進入按指紋,輸入iTunes Store帳密的程序。

StoreKit說明,當SKPayment加入到SKPaymentQueue後,開發者需要實作一個adoptSKPaymentTransactionObserverprotocol的物件,並加入到SKPaymentQueue裡。之後這個Observer就是負責處理各種交易結果。比如成功時就要開啟對應的功能等。

所以這個Class就取名叫StoreObserver,職責是實作SKPaymentTransactionObserverprotocol,處理交易完成的後續行為。並負責與SKPaymentQueue互動,比如購買,取回過去的購買紀錄。最後可方便的在任何地方發動購買,然後在需要的地方容易接收到結果。

基於以上需求,開去構思這個模組。

先處理交易的部分,這就是單純與StoreKit串接。以下兩個Public方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 購買SKProduct
// Create and add a payment request to the payment queue
-(void)buy:(SKProduct *)product
{
    SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
  [[SKPaymentQueue defaultQueue] addPayment:payment];
}

// 取回過去完成交易的非消耗品購買與自動續訂紀錄
-(void)restore
{
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}

實作上會把這個StoreObserver做成Singleton,因為APP裡面可能會有許多頁面允許付款,只要[[StoreObserver sharedInstance] buy:product];就可以購買商品。然後因應上面的restore方法,這裡需要一個NSArray來裝取回的商品們,命名為productsRestored

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ (StoreObserver *)sharedInstance
{
    static dispatch_once_t onceToken;
    static StoreObserver * storeObserverSharedInstance;
    
    dispatch_once(&onceToken, ^{
        storeObserverSharedInstance = [[StoreObserver alloc] init];
    });
    return storeObserverSharedInstance;
}


- (instancetype)init
{
  self = [super init];
  if (self != nil)
    {
        _productsRestored = [[NSMutableArray alloc] initWithCapacity:0];
    }
  return self;
}

這邊重要的來了,如果程式的某個地方呼叫了StoreObserverbuy發法,之後StoreObserver收到交易完成的資訊,而APP裡面可能會有很多地方因為這個交易而產生UI上的變化,比如買了一部影片,影片要開始播放,影片櫃需要新增新影片,會員的購買紀律需要增加一筆。該怎麼通知那麼多地方?這裡用了NSNotificationCenter

為什麼呢?因為在上述一對多的狀況下,StoreObserver會被設計成它不關心那些畫面或地方需要這些訊息。但它會把資料準備好,接著廣播。

當通知的人不關心他會通知到誰,但可能需要被通知的人很多時,NSNotificationCenter機制就派上用場了。

1
NSString * const TNIAPPurchaseNotification = @"TNIAPPurchaseNotification";

再來是讓接收廣播的地方容易做處理。在這個Class裡面我們設置三個Property,statusmessagepurchasedID。當這個Class實作SKPaymentTransactionObserver時,根據收到的資訊做整理,讓接收的人可以很方便利用交易結果。

1
2
3
4
5
6
7
8
9
10
11
typedef NS_ENUM(NSInteger, TNIAPPurchaseNotificationStatus)
{
    TNIAPPurchaseFailed, // Indicate that the purchase was unsuccessful
    TNIAPPurchaseSucceeded, // Indicate that the purchase was successful
    TNIAPRestoredFailed, // Indicate that restore products was unsuccessful
    TNIAPRestoredSucceeded // Indicate that restore products was successful
};

@property (nonatomic) TNIAPPurchaseNotificationStatus status;
@property (nonatomic, copy) NSString *purchasedID;
@property (nonatomic, copy) NSString *message;

比如當我們實作的SKPaymentTransactionObserver方法被SKPaymentQueue呼叫時,整理一下再POST Notification出去

1
2
3
4
5
6
7
8
9
10
// Called when an error occur while restoring purchases. Notify the user about the error.
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
{
    if (error.code != SKErrorPaymentCancelled)
    {
        self.status = IAPRestoredFailed;
        self.message = error.localizedDescription;
        [[NSNotificationCenter defaultCenter] postNotificationName:TNIAPPurchaseNotification object:self];
    }
}

實際運作

這邊我們不考慮要怎麼取得到SKPayment物件,因為這部分邏輯不在StoreObserver負責範圍內。

在任何地方容易的發動購買

1
2
3
SKProduct *product = (SKProduct *)productRequestResponse[indexPath.row];
        // Attempt to purchase the tapped product
        [[TNStoreObserver sharedInstance] buy:product];

在需要的地方容易收到並做處理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handlePurchasesNotification:)
                                                 name:TNIAPPurchaseNotification
                                               object:[StoreObserver sharedInstance]];

// Update the UI according to the purchase request notification result
-(void)handlePurchasesNotification:(NSNotification *)notification
{
    StoreObserver *purchasesNotification = (StoreObserver *)notification.object;
    
    IAPPurchaseNotificationStatus status = (IAPPurchaseNotificationStatus)purchasesNotification.status;
    NSString *message = purchasesNotification.message;
    NSString *purchasedID = purchasesNotification.purchasedID;
   

  switch (status)
    {
        case IAPPurchaseFailed:
            //購買失敗...
          break;
            
        case IAPDownloadSucceeded:
        {
            //購買成功...
        }
          break;
        
        case IAPRestoredSucceeded:
        {
            //回復成功...
        }
            break;
            
        case IAPRestoredFailed:
            //回復失敗...
            break;
  } 
}

Comments