[C#][.NET][WinUI 3] Microsoft Store に公開するアプリでトライアル・アドオンを管理する

Microsoft Store に有料アプリ(サブスク・買い切り・有料プラン・トライアル付きなど)を配布するにあたって、アプリ・アドオンの購入状態をストアから取得する必要があります。

アドオンについて

アドオンはアプリ本体以外の購入要素です。
有料プランの加入や開発者への寄付、サブスクリプションの加入、ガチャ石の購入といった要素に該当します。

アプリ本体の購入はMicrosoft Store上で行うのですが、アドオンの購入はアプリ側からリダイレクトします。
Microsoft Store 上には項目は表示されません。 されるかも🙄
アプリから購入画面にリダイレクトする場合、細かい支払い云々の画面はStore側で表示してくれるので、アプリ側から行うことは購入のリクエスト(RequestPurchaseAsync)をするだけです。(購入後の処理云々はまた別でありますが)

ストアから情報の取得

using Windows.Services.Store;

// 現在のユーザのストア情報を取得
StoreContext context = StoreContext.GetDefault();

StoreContext.GetDefault() は現在のユーザの情報を取得します。

ちなみに StoreContext から取得できる情報は下記の通りです。

  • ライセンス情報の取得
    • アプリやアドオンが購入済みかどうか
    • 試用版かフルライセンスか
    • コンシューマブル(消費型)アドオンの残量など
  • 購入処理
    • アプリ内課金(IAP)の実行
    • アドオンやサブスクリプションの購入
    • 購入結果の取得(成功・失敗・既に購入済みなど)
  • パッケージ更新
    • Microsoft Store 経由でのアプリ更新の確認・ダウンロード・インストール
    • 製品情報の取得
    • アプリやアドオンの Store ID に基づく詳細情報(価格、説明、画像など)
  • ユーザーコンテキストの切り替え
    • GetForUser(User user) によるマルチユーザー対応

今回は上記のうち、上の 2 つについてまとめます。

ライセンス情報の取得

// ライセンス情報の取得
StoreAppLicense license = await context.GetAppLicenseAsync();

アプリ自体のライセンス有効状態の確認

license.IsActiveライセンスの有効状態
購入済みまたは試用期間中(無料含む)の場合に True になる
license.IsDiscActiveディスクライセンスの有効状態
正規のライセンスを持ったインストールディスクからインストールされた場合に True になる
license.ExpirationDateライセンスの有効期限
※ 基本的にStoreAppLicenseクラスにおいてはあまり意味を持たない
license.ExtendedJsonDataライセンスの詳細情報(JSON形式)
license.SkuStoreIdライセンスのSKUのユニークなID
(正直あまりよくわかっていない)

IsActive 以外はまあそうそう使うことはないでしょう。

アプリのトライアル状態の取得

license.IsTrial現在のユーザが試用期間中なら True 。
(同じ Windows ユーザで?)Microsoft Store にサインインしているアカウントが違くても True になる。
※ 無料アプリに対してTrialを設定しても True にはならない
license.IsTrialOwnedByThisUserMicrosoft Store にサインインしているアカウントが試用期間中なら True 。
※ 無料アプリに対してTrialを設定しても True にはならない
license.TrialTimeRemaining試用期間の残り時間。
license.TrialUniqueId試用ライセンスのユニークなID

厳密に判断したいのであれば IsTrialOwnedByThisUser を利用するのがよさそうです。

アドオン

まず、アドオンには以下のような種類があります。

開発者管理の消費型アドオン
Developer-managed consumable
何回でも購入可能。
開発者がアプリ側で残高(購入したアドオンの残量)の消費タイミングを決定する。
ユーザはアプリ側から消費報告(フルフィルメント報告)を受けないと再購入できない。
ストアは購入して消費されていない個数を保持している。
ストア管理の消費型アドオン
Store-managed consumable
何回でも購入可能。
購入後、ストアがすぐに消費処理するので、実装が用意な反面、消費タイミングを決定できない。
ストアはすぐに消費処理するため、ユーザはすぐにアドオンの再購入が可能。また、残量は管理されない。
Windows 10 version 1607 以降のユーザのみサポート。
アプリは Windows 10 SDK version 14393 以降でビルドする必要がある。
買い切り型アドオン
Durable
一度購入したら永久的または設定された期限まで保有されるアドオン。
アドオンのライセンスが切れるまで再購入不可。
サブスクリプション型アドオン
Subscription
自動で定期購入されるアドオン。
Windows 10 version 1607 以降のユーザのみサポート。
アプリは Windows 10 SDK version 14393 以降でビルドする必要がある。

各アドオンの購入・消費処理で用いるメソッドは下記の通りです。ここで、下記の storeId はMicrosoft Partner Center (ストアアプリの管理サイト) でアドオン登録時時に自動生成される 9NBLGGHXXXXX のような12桁の固有IDです。
※ 最低限のメソッドのみ記載しています。

context.RequestPurchaseAsync(string storeId)アドオンを購入するメソッド。
いずれの型のアドオンの購入においてもこのメソッドを使用する。
戻り値 result.Status で購入の成否やすでに購入済みかを判断する。
context.GetConsumableBalanceRemainingAsync(string storeId)開発者管理消費型アドオンの残高を取得する。
実際の残高(購入数)は 戻り値の BalanceRemaining メンバが保持している。
context.ReportConsumableFulfillmentAsync(string productStoreId, uint quantity, Guid trackingId)開発者管理消費型アドオンのフルフィルメント報告(残高の消費報告)をするメソッド。
戻り値 result.Status で報告の成否を判断する。
引数に使用する trackingId は一意のIDである必要がある。
同じGUIDで報告した場合、過去の報告分を参照して消費済みであれば成功を返す。
そのため、GUIDが重複すると後から購入した分の消費報告が行われず、再購入ができなくなってしまう。

Guid.NewGuid() でGUIDを生成すればまず重複しません。
仮に重複したのであれば 1 / (2^122) の確率、つまり約 53 無量大数分の 1 を引いたラッキーボーイ/ガールだと喜びましょう。

開発者管理消費型アドオンの利用

開発者管理消費型アドオンでは、残高の管理はストア側で、消費報告はアプリ側で行います。

先に機能開放を行うか、消費処理を行うか、これはどちらでも構いません。単調な実装ではどちらにも欠点はあります。

一番シンプルな実装をすると下記のようになります。実際は戻り値チェックや起動時の残量チェックなどがあったほうが Better だとは思いますが、いったん最低限の実装です。

private async Task<bool> PurchaseDeveloperManagedConsumableAddOn(string storeId)
{
    // ストアからコンテキストを取得
    StoreContext context = StoreContext.GetDefault();
    // 購入リクエストを送信
    StorePurchaseResult result = await context.RequestPurchaseAsync(storeId);

    // 購入結果を確認
    if (result.Status != StorePurchaseStatus.Succeeded && result.Status != StorePurchaseStatus.AlreadyPurchased)
    {
        // 購入失敗または接続エラー
        return false;
    }

    // 以前購入済みで、何らかの理由で購入後の処理が完了していない(=買ったのに機能開放されていない)場合
    if (result.Status == StorePurchaseStatus.AlreadyPurchased)
    {
        // ユーザーに通知
        ...
    }

    // 購入数量を取得(通常は1個だが、アプリクラッシュなどで未消費の購入済みデータが複数存在する可能性がある)
    StoreConsumableResult balanceResult = await context.GetConsumableBalanceRemainingAsync(storeId);

    for (uint i = 0; i < balanceResult.BalanceRemaining; i++)
    {
        // ガチャ石チャージなど
        ChargeStone();
    }

    // 購入済みデータを消費
    Guid trackingId = Guid.NewGuid();
    await context.ReportConsumableFulfillmentAsync(storeId, balanceResult.BalanceRemaining, trackingId);

    return true;
}

ストア管理消費型アドオン利用

ストア管理消費型では、ストアは残数を保持せず、購入されたらすぐに消費という扱いになります。そのため、アプリ側での消費処理も不要です。

ただし、購入後に機能開放やガチャ石チャージなどの最中にアプリクラッシュが発生するとお金を払っただけの状態になったりしてしまう可能性があります。

シンプル場実装をした場合、以下の通りになります。

private async Task<bool> PurchaseStoreManagedConsumableAddOn(string storeId)
{
    // ストアからコンテキストを取得
    StoreContext context = StoreContext.GetDefault();
    // 購入リクエストを送信
    StorePurchaseResult result = await context.RequestPurchaseAsync(storeId);

    // 購入結果を確認
    if (result.Status != StorePurchaseStatus.Succeeded)
    {
        // 購入失敗または接続エラー
        return false;
    }
    
    // ガチャ石チャージなど
    ChargeStone();

    return true;            
}

開発者管理消費型と比べて一気にシンプルになりましたね。
簡単な実装にしたいのであればこれでいいと思います。

買い切り型・サブスクリプション型アドオンの利用

買い切り型およびサブスクリプション型アドオンの一覧は license.AddOnLicenses から取得することができます。ここで、下記の "storeId"RequestPurchaseAsync などと同じIDです。

StoreLicense addonLicense = license.AddOnLicenses["storeId"];
addonLicense.ExpirationDateライセンスの有効期限
addonLicense.ExtendedJsonDataライセンスの詳細情報(JSON形式)
addonLicense.InAppOfferToken開発者がアドオンに対して設定する識別子
addonLicense.IsActiveライセンスの有効状態
購入済みまたは試用期間中(無料含む)の場合に True になる
addonLicense.SkuStoreIdライセンスのSKUのユニークなID

アプリと同様に IsActive でライセンス有無をチェックできそうですね。

買い切り型やサブスクリプション型アドオンは基本的に Pro 版などの機能開放目的に使用されたりします。
そのため、起動時に以下のようにライセンスのチェックを行います。

private async Task CheckLicense(string storeId)
{
    // ストアからコンテキストを取得
    StoreContext context = StoreContext.GetDefault();
    // アプリのライセンス情報を取得
    StoreAppLicense license = await context.GetAppLicenseAsync();
    // 指定されたストアIDのアドオンライセンスが存在するか確認
    if (license.AddOnLicenses.TryGetValue(storeId, out StoreLicense? addOnLicense))
    {
        if (addOnLicense.IsActive)
        {
            // 機能開放など
            UnlockProEdition();
        }
    }
}

購入処理はほかアドオン同様 RequestPurchaseAsync でできるので、購入成功したら GetAppLicenseAsync で最新のライセンス状態を取得して機能開放などを行っていけばいいと思います。

RequestPurchaseAsync の実行が終了した直後であっても GetAppLicenseAsync で正しいライセンスとは限りません。Store側の同期の問題で、反映まで最大で数十秒とかかかるかもしれません。(未確認)

あとがき

実はここまで、実動作確認していません。(は?)
Store を使用してのテストのやり方を調べている最中なので許してください。
間違いがあれば適宜修正していきます。

投稿者 ぶどうじゅーす【公式】

多趣味な人です

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です