UWP - B2B 確認 In-app 購物記錄

上一篇 UWP - in-app product purchases 介紹了整合内部購買的機制。本篇介紹怎麽做 B2B 確認用戶購買是否成功。

開始介紹 B2B 的開發步驟前,請先閲讀兩個重要的文件:

  1. Windows-universal-samples/Samples/Store/:重要的範例
  2. 附加元件提交:幫助瞭解 Add-ons 的類型與流程
    • Windows 10 (1607 以上)支援新的類型:Subscription (訂閲)。搭配 Windows.Services.Store 使用。
    • Subscription 可以設定每次續約的周期/價格,是否有試用周期,開放購買的對象(beta 測試會需要)。
    • 需注意價格預設是免費,發佈之後就不能再往上調整,只能往下調整,所以第一次設定時要考慮好定價。
    • 用戶可以隨時從 Microsoft Account 中取消訂閲。
    • 可以參考 Test your in-app purchase or trial implementation 進行測試。

MS IAP APIs 提供 RSET 幫助 B2B 管理用戶的 in-app 產品:

  1. Microsoft Store collection API:查詢用戶已經購買的產品與回報消費性產品已經消費完畢。
  2. Microsoft Store purchase API:指定用戶享受免費產品,抓到用戶已經訂閲的產品與改變訂閲用戶的付款狀態

詳細可參考:Manage product entitlements from a service

如何使用 Collection API 與 Purchase API,以及 Store 與自己開發的 Service 是怎麽運作的呢?

利用下圖描述: 上圖的分成兩大部分:

  • For Service:
    1. 先在把 my billing server domain 註冊到 Azure AD 變成 Azure AD Web application,來拿到 tenant_id 與 client_id (或稱 Application ID),細部設定參考: Configure an application in Azure AD
    2. 把 client_id 加入到 Windows Dev Center 中該 App 的訂閲服務關聯設定
    3. 在 my billing server 加入負責建立 AD Access Token 與綁定來自 App 的 MS Store ID 的邏輯,關於建立 AD Access token 的邏輯,可以參考下方的説明
    4. 負責保存與更新 MS Store ID (MS Store ID 有 90 天使用期限,需利用 Renew MS Store ID Key 在 Server 幫用戶更新)
    5. 操作 Collection APIs 檢查用戶購買的產品或是 Purchase APIs 抓取用戶的訂閲期限與更新帳單資訊
  • For Client:
    1. 由於建立 MS Store ID 需要 AD Access Token (collection/purchase),所以需要先 my billing server 請求資料
    2. 搭配 StoreContext.GetCustomerCollectionsIdAsyncStoreContext.GetCustomerPurchaseIdAsync 將拿到的兩種 AD Access Token 分別建立對應的 MS Store ID,並回報給 my billing server 保存
    3. 利用 StoreContet 購買 Add-ons 產品 (需注意如果設備沒有登入 MS Account 是無法購買的)

要完成 Client/Server 整合才能操作 purchase/collection APIs,如下步驟:

Step1: 抓取用到的 AD Access token,主要分成 3 種

  • https://onestore.microsoft.com : 一定要建立的,因爲它被用在向 REST APIs 發出請求時,放在呼叫 Collection APIs 或 Purchase APIs 的 HTTP Header authorization 驗證值;
  • https://onestore.microsoft.com/b2b/keys/create/collections:要存取 collections 系列的 APIs,需要建立它的 Access token,並傳給 Client 建立 MS Store ID,才能代表 User 操作 Collection APIs;
  • https://onestore.microsoft.com/b2b/keys/create/purchase:要存取 purchase 系列的 APIs,需要建立它的 Access token,並傳給 Client 建立 MS Store ID,才能代表 User 操作 Purchase APIs;

詳細可參考 Step 5: Call the Microsoft Store collection API or purchase API from your service

如下程式,分別建立 3 種 Access Token,並回傳給 Client:

[HttpGet]
public async Task Get()
{
    // 1. get header AAD access token
    AuthResultData result = new AuthResultData();
    result.Auth = await GetAzureADAccesToken(AuthType.Auth);
    result.Collection = await GetAzureADAccesToken(AuthType.Collection);
    result.Purchase = await GetAzureADAccesToken(AuthType.Purchase);

    return result;
}

private async Task GetAzureADAccesToken(AuthType type)
{
    string tenantId, clientId, clientSecret;
    string resource = string.Empty;

    switch (type)
    {
        case AuthType.Collection:
            resource = "https://onestore.microsoft.com/b2b/keys/create/collections";
            break;
        case AuthType.Purchase:
            resource = "https://onestore.microsoft.com/b2b/keys/create/purchase";
            break;
        default:
            resource = "https://onestore.microsoft.com";
            break;
    }

    FormUrlEncodedContent postContent = new FormUrlEncodedContent(new Dictionary
    {
        { "grant_type", "client_credentials" },
        { "client_id", clientId },
        { "client_secret", clientSecret },
        { "resource", resource },
    });

    postContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");

    using (HttpClient client = new HttpClient())
    {
        var response = await client.PostAsync($"https://login.microsoftonline.com/{tenantId}/oauth2/token", postContent);

        if (response.StatusCode != System.Net.HttpStatusCode.OK)
        {
                    return string.Empty;
        }

        var responseContent = await response.Content.ReadAsStringAsync();
        var jsonObject = JsonConvert.DeserializeObject(responseContent);
        return jsonObject.access_token;
    }
}

Step2: Client App 向 my billing server 拿到 collection/purchase APIs 的 Access Token,產生對應的 MS Store IDs 並回傳給 server

private async Task GenerateMicrosoftStoreID()
{
    // Request my billing server to get collection/purchase API access token
    var authResult = await GetTokenFromAzureOAuthAsync();
            
    // publisherUserId is identify user on your server, such as: serial id, not Microsoft Account
    string publisherUserId = "poumason@live.com";

    // Generate collection / purchase Id must using difference access token
    var collectionStoreId = await storeContext.GetCustomerCollectionsIdAsync(authResult.Collection, uid);
    var purchaseStoreId = await storeContext.GetCustomerPurchaseIdAsync(authResult.Purchase, uid);

    // Report to my billing server to keep MS Store ID
    var actionData = new PostActionData()
    {
        UID = uid,
        AuthData = authResult,
        CollectionStoreID = collectionStoreId,
        PurchaseStoreID = purchaseStoreId
    };

    HttpClient client = new HttpClient();
    var content = new HttpStringContent(actionData.Stringify());
    content.Headers.ContentType = new HttpMediaTypeHeaderValue("application/json");
    var result = await client.PostAsync(new Uri("http://mybillingserver.azurewebsites.net/api/inapps"), content);

    var responseContent = await result.Content.ReadAsStringAsync();
}

Step3: my billing server 呼叫 collection / purchase APIs 獲取用戶已經購買的或訂閲的商品

取得用戶已經訂閲的商品,API 用法可以參考 Get subscriptions for a user

private async Task GetSubscription(string accessToken, string storeID)
{
    var purchase = new PurchaseQueryData
    {
        B2BKey = storeID
    };

    StringContent postContent = new StringContent(JsonConvert.SerializeObject(purchase));
    postContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");

    using (HttpClient client = new HttpClient())
    {
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        client.DefaultRequestHeaders.Host = "purchase.mp.microsoft.com";

        var response = await client.PostAsync("https://purchase.mp.microsoft.com/v8.0/b2b/recurrences/query", postContent);

        if (response.StatusCode != System.Net.HttpStatusCode.OK)
        {
            return string.Empty;
        }

        var responseContent = await response.Content.ReadAsStringAsync();

        return responseContent;
    }
}

取得用戶已經購買的商品,API 用法可以參考 Query for products

private async Task QueryOfProduct(string accessToken, string storeID, string uid)
{
    var collection = new CollectionData();
    collection.Beneficiaries.Add(new UserIdentityData
    {
       Reference = uid,
       Value = storeID
    });
    collection.ProductTypes.Add("Application");
    collection.ProductTypes.Add("Durable");
    collection.ProductTypes.Add("UnmanagedConsumable");
    
    StringContent content = new StringContent(JsonConvert.SerializeObject(collection));
    content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

    using (HttpClient client = new HttpClient())
    {
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        client.DefaultRequestHeaders.Host = "collections.mp.microsoft.com";
        var response = await client.PostAsync(new Uri(https://collections.mp.microsoft.com/v6.0/collections/query), content);

        if (response.StatusCode != System.Net.HttpStatusCode.OK)
        {
            return string.Empty;
        }

        var responseContent = await response.Content.ReadAsStringAsync();

        return responseContent;
    }
}

再强調一次,從 server 要呼叫 collection / purchase APIs 時, HTTP Request Header 中 Authorization 需給與來自 https://onestore.microsoft.com 的 Access Token,而 APIs 内給的 body 就分別給與 MS Store ID 來代表用戶。

分別在 Client App 與 Server 完成上面的事情,既可以讓 Server 拿到 MS Store ID (代表用戶) 來查詢該用戶購買的資訊,完成一些檢驗的流程,例如:購買確認,發票開立等。

更多其他的說明,可以參考 Manage product entitlements from a service

[補充]

  • Claims in a Microsoft Store ID key
    利用 JSON Web Token (JWT) 格式建立,裏面包含許多資料,其中 http://schemas.microsoft.com/marketplace/2015/08/claims/key/userId 最重要,因爲它不是 Microsoft Account,它代表這個用戶在你的 server 中的識別值,它來自于 StoreContext.GetCustomerCollectionsIdAsync 或 StoreContext.GetCustomerPurchaseIdAsync 方法中所給予的 publisherUserId。

======

B2B 整合 MS IAP APIs 其實不困難,最麻煩是 時間差。 在 Windows Dev Center 加入 add-on 產品與收到 email 通知上架後,需再等大約 16 小時之後,才能在 StoreContext 看到資料。同樣地,整合 APIs 在註冊與 App 關聯之後也要等超過 16 小時才能拿到資料。所以建立在開發前,建議先把要販售的商品/訂閲,Azure AD 註冊 web application,以及關聯 Windows Dev Center 都設定好,過 1 天之後再開發會比較完整。

References: