UWP - in-app product purchases

如果 App 裏面是想要賣一些產品 (一次性或是租用性) 的話,可以參考這一篇的介紹幫助您賺點錢。

[事前準備]

  1. 請完成 Dev Center 中 Account 的付費驗證,請參考<Getting paid>填寫必要的資料跟匯款的賬號/銀行
  2. 在 Dev Center 建立一個 Project,利用 Visual Studio 將專案與 Project 綁定抓到 publisher 資訊 (Packaging Universal Windows apps for Windows 10)

[注意]

App 要提供 in-app purchase 與 trial functionality 的話,有些功能會基於 Windows 10 的版本。 不同的 Windows 10 使用不同的 namespaces 將 in-app 購買和試用功能加入 app:

  • Windows.Serivce.Store 如果 App 目標版本是 Win10 1607 (14393) 以上建議使用它開發 in-app 與 trials 功能。 支援最新 add-on types 管理與 Windows Dev Center 或 Store 的整合。
  • Windows.ApplicationModel.Store 所有的 Win10 都支援這個舊的 APIs,不過該 namespace 不會再更新支援最新的產品類型。
 
本文針對 Windows.Service.Store 爲主來説明,UWP app 能提供的 add-ons 類型,如下:
Add-on type Description
Durable 永久性產品。 預設該類型的產品沒有期限,用戶衹能購買一次。如果有特別指定過期時間,用戶可在過期後再次購買。
Developer-managed consumable 產品可以被購買,使用與再次購買。此類型通常別用在程式中的貨幣。 此類型的產品在用戶購買之後,開發者需要在 App 或 Service 維持用戶還有多少量可以消費,直到用戶消費滿額之後才可以再次購買
Store-managed consumable 類型同 Developer-managed consumable。 不同點在於,是由 Store 負責維持用戶還有多少量可以消費。用戶可以隨時查詢自己可用的量。 Store-managed consumable 從 Windows 10, version 1607 才開始支援。所以 App 也要使用對應的版本跟使用 Windows.Service.Store

Windows.Service.Store 重要的元素:

  • StoreContext 可用它取得 app 相關資訊,例如:可用的 app-ons, license, 等,與該 user 在目前 app 已經購買的項目等其他功能。

如果 App 衹能有一個用戶使用(大部分的 App 都是的單一用戶使用)可以使用下面的指令連結到用戶在 Store 上的資料(GetDefault()):


Windows.Services.Store.StoreContext context = StoreContext.GetDefault();

如果 App 允許多個用戶登入使用(multi-user app),要改用:GetForUser() 搭配指定的用戶 Index 來取得。


var users = await Windows.System.User.FindAllAsync();
Windows.Services.Store.StoreContext context = StoreContext.GetForUser(users[0]);

拿到 StoreContext 之後就可以往下操作需要的任務。 1. 在 Dev Center 建立 add-ons 的產品:(使用單一用戶爲例)

分別建立 耐久性 與 由市集管理的消耗性項目。詳細的填寫資料可以參考<Add-on submissions>。 建立完成之後 (需要等待 add-ons 的審核通常衹要 1 小時内就會通過),利用 Visual studio 中的 Associate App with Store 把專案綁定,才能使用相同的 App Id 拿到該 App 下有建立的 add-ons。 2. 抓出系統裏面可使用的用戶,挑選要抓去那個用戶在這個 App 已購買或可以購買的内容


public async Task Initialize()
{
     // 抓取用戶,并用 UserDataWrapper 包裝起來給 ComboxControl 使用
     var users = await Windows.System.User.FindAllAsync();

     int i = 1;
     foreach (var item in users)
     {
         this.Users.Add(new UserDataWrapper(item, $"user{i}"));
         i++;
     }
}

public class UserDataWrapper
{
     public User Self { get; private set; }

     public string Id { get; private set; }

     public UserDataWrapper(User user, string id)
     {
         Self = user;
         Id = id;
     }
}

3. 利用 StoreContext 查詢可以購買的產品: 利用 GetAssociatedStoreProductsAsync 取得該 App 可使用的所有 add-ons 資訊 (StoreProduct)。 如果有很多 add-ons 的内容可以改用 GetAssociatedStoreProductsWithPagingAsync 取得,另外產品類型多樣可以藉由 ProductKind 加以分類。


public async Task GetProducts(object sender, RoutedEventArgs e)
{
      /*
       *  ProductKind:
       *    Application,
       *    Game,
       *    Consumable, (Store-managed)
       *    UnmanagedConsumable, (Developer-managed)
       *    Durable
      */

      string[] filter = new string[] { "Durable", "Consumable" };
      var products = await storeContext.GetAssociatedStoreProductsAsync(filter);

      if (products.ExtendedError != null)
      {
          Errored?.Invoke(this, products.ExtendedError);
          return;
      }

      Products.Clear();

      foreach (var item in products.Products)
      {
           Products.Add(new StoreProductDataWrapper(item.Key, item.Value));
      }
}

爲了在 ListView 可呈現利用 StoreProductDataWrapper 包裝起來。 3. 查詢目前 App 的授權與 add-ons 已經購買的項目: 利用 GetUserCollectionAsync 查詢用戶已經購買且可以使用的項目。如果資料太多可以搭配 GetUserCollectionWithPagingAsync 使用。


public async Task GetPurchasedProducts(object sender, RoutedEventArgs e)
{
      // 抓取已經購買的項目
      var purchased = await storeContext.GetUserCollectionAsync(GetProductKinds());

      if (purchased.ExtendedError != null)
      {
           Errored?.Invoke(this, purchased.ExtendedError);
           return;
      }

      if (purchased.Products == null || purchased.Products.Count == 0)
      {
           Message = "Not purchased any product";
           return;
      }

      PurchasedProducts.Clear();
            
      foreach (var item in purchased.Products)
      {
           PurchasedProducts.Add(new StoreProductDataWrapper(item.Key, item.Value));
      }
}

如果沒有買過任何 add-ons 的 products 會抓不到内容。可以配合下一步購買產品,不過要記得設定是 free 的避免付費。 4. 購買產品: 利用步驟 2 可以抓出可以購買的產品 (StoreProduct),利用 RequestPurchaseAsync()。訂購結果裏面會有 StorePurchaseStatus 確定是否購買成功。呼叫 RequestPurchaseAsync() 要在 UI thread 使用,避免發生錯誤 (0x800706be: This value corresponds to the RPC_S_CALL_FAILED error code)。


public async void PurchaseProduct(object sender, RoutedEventArgs e)
{
      var dataWrapper = listView.SelectedItem;

      if (dataWrapper != null && dataWrapper.Product != null)
      {
         // 購買產品
         var product = dataWrapper.Product;
         if (product.IsInUserCollection == false)
         {
              var result = await product.RequestPurchaseAsync();

              if (result.ExtendedError != null)
              {
                  Errored?.Invoke(this, result.ExtendedError);
                  return;
              }

              switch (result.Status) 
              {
                  case StorePurchaseStatus.Succeeded:
                       // purchase successed
                       break;
                  case StorePurchaseStatus.AlreadyPurchased:
                       break;
                  case StorePurchaseStatus.NetworkError:
                  case StorePurchaseStatus.NotPurchased:
                  case StorePurchaseStatus.ServerError:
                       break;
              }
         }
     }
}

如果您知道 StoreId 的話,也可以利用 StoreContext 進行購買,如下:


StorePurchaseResult result = await context.RequestPurchaseAsync(storeId);

5. 購買消費性產品 使用的 Store-managed consumable 會比較方便,在 Dev Center 設定時就需要設定每次購買給予到少額度,App 利用 GetConsumableBalanceRemainingAsync 取得可用的額度,在 App 使用之後再透過 ReportConsumableFulfillmentAsync 告訴 Store 用了多少額度。直到用完額度,用戶才可以再次購買。


// 利用 GetConsumableBalanceRemainingAsync 取得可以用的額度
string addOnStoreId = "9p4rrvhmg63x";
StoreConsumableResult result = await context.GetConsumableBalanceRemainingAsync(addOnStoreId);

相反地,如果使用 Developer-managed consumable 就需要在向 StoreContext 或 StoreProduct 訂購成功後,自己管理可用的 quantity, 等到該用戶使用完畢所有的 quantity 時,藉由 ReportConsumableFulfillmentAsync() 通知 Store 已經消費完畢,這樣用戶才能再購買下一次。


// addOnStoreId: 代表產品的 StoreId
// quantity: 如果是 developer-managed consumable , 給 1 就好。
// trackingId: 用來代表交易,可直接使用 GUID 
uint quantity = 1;
string addOnStoreId = "9p4rrvhmg63x";
Guid trackingId = Guid.NewGuid();
    
StoreConsumableResult result = await context.ReportConsumableFulfillmentAsync(addOnStoreId, quantity, trackingId);

更多消費性產品的購買可以參考 Enable consumable add-on purchases。 [重要元素]

Name Description
OfflineLicensesChanged Raised when the status of the app's license changes (for example, the trial period has expired).
GetAppLicenseAsync Gets license info for the current app, including licenses for add-ons for the current app.
GetAssociatedStoreProductsAsync Gets the list of products that can be purchased from within the current app.
GetAssociatedStoreProductsWithPagingAsync Gets the list of products that can be purchased from within the current app. This method supports paging to return the results.
GetConsumableBalanceRemainingAsync Gets the remaining balance for the specified consumable add-on for the current app.
GetCustomerCollectionsIdAsync Retrieves a Windows Store collections ID key that can be used to query for product entitlements or to consume product entitlements that are owned by the current user.
GetCustomerPurchaseIdAsync Retrieves a Windows Store purchase ID key that can be used to grant entitlements for free products on behalf of the current user.
GetStoreProductForCurrentAppAsync Gets Windows Store listing info for the current app and provides access to a method that you can use to purchase the app for the current user. (取得現在 App 的所有資訊)
GetStoreProductsAsync Gets Windows Store listing info for the specified products that can be purchased from within the current app. (抓取指定 Store IDs 的資訊)
GetUserCollectionAsync Gets Windows Store info for the add-ons of the current app for which the user has entitlements to use.
GetUserCollectionWithPagingAsync Gets Windows Store info for the add-ons of the current app for which the user has entitlements to use. This method supports paging to return the results.
ReportConsumableFulfillmentAsync Reports a consumable add-on for the current app as fulfilled in the Windows Store.
RequestPurchaseAsync(String) Requests the purchase for the specified app or add-on and displays the UI that is used to complete the transaction via the Windows Store.
RequestPurchaseAsync(String,StorePurchaseProperties) Requests the purchase for the specified app or add-on and displays the UI that is used to complete the transaction via the Windows Store. This method provides the option to specify additional details for a specific offer within a large catalog of products that are represented by a single listing in the Windows Store, including the product name to display to the user during the purchase.
代表現在 App 的 license,也包括了在 App 裏面購買的所有 products license。重要的屬性:AddOnsLicenses 是個 Dictionary<string, StoreLicense> 集合。
 
代表現在 App 裏面已經購買的相關授權。
代表 Windows Store 中可被使用的產品 (可以得到在 Dev center 設定該產品的所有資訊)。
  • Products, SKUs, and availabilities 3者的關係
Object type Description
StoreProduct 代表在 Store 裏任何類型的產品,如:App 或是 add-ons。 本身具有定價資訊,介紹圖片/影片,Store ID 等相關屬性,可直接利用它提供的方法進行購買。 Store ID 具有 12 個字元 (12-character alpha-numeric string, such as 9NBLGGH4R315), 可以在 Dev Center 管理該產品的時候看到,通常會被稱為:product Store ID。
StoreSku SKU 代表特定版本的產品,擁有各自描述,定價與唯一的產品資訊。App 或 add-on 都會有一個預設的 SKU。 衹有在 App 上架到 Store 有分成試用版跟完整版的時候才會多個 SKU (相同的 app 不同的版本就有不同的 SKU)。 部分開放商會定義自己的 SKUs,例如:游戲開發商會定義在 SKU 的屬性某些市場的内容有所不同, 或是對發佈的數位影片内容一個 SKU 給高清,一個 SKU 給 4K。 每一個 product 都具有一個 SKUs 屬性,可以利用它來取得相關資訊。 SKU 的 Store ID 是 /xxxx , xxxx 代表產品 SKU 的識別。通常稱爲 SKU Store ID。
StoreAvailability availability 代表 SKU 是否可用。代表 SKU 的特定版本有自己的定價資訊。 SKU 預設有一個 availability。開發者可以調整同一個 SKU 有不同的價格跟有效性。 availability 的 Store ID 是 /xxxx/yyyyyyyyyyyy , xxxx 代表 SKU , yyyyyyyyyyyy 代表 SKU 的 availability 。 通常稱爲 availability Store ID。
[補充]
  • add-ons 購買使用的是 Microsoft account (也就是登入市集的帳號),如果 App 裏面使用的是不同的 email 但是代表同一人,需要額外處理把兩個對應提供給自己的 service 來做購買確認。
  • Windows.Services.Store namespace 沒有支援模擬 license 的方式,要進行測試 App 需要先送到 Dev Center 再設定隱藏起來,才有辦法進行測試。
    • 上架完畢後要記得在開發環境透過 Store 下載並安裝,最好也啓動過一次讓 license 能正確被安裝到系統裏。
  • 如果開發是 Server 端要確定用戶是否真的在 Store 購買成功的話,需要參考在 Windows Store collection REST API 中的 query for products method
    • API 的授權需要依賴 Azure AD authentication

======

已經不太流行讓用戶直接一次買斷 App (游戲或是特殊用途除外),通常會先讓用戶下載後,再慢慢針對一些功能的開關或是廣告是否出現來請對方付費。 可參考上面介紹的方式改變一下銷售的方式看有沒有辦法增加自己的荷包。希望對大家有點幫助。 References: