Microsoftストアでは、しばらく前からアプリのアドオンのサブスクリプション販売が可能になっています。アプリ自体を販売する場合は、アプリ側の対応はそれほど手間ではないですが、アドオン(アプリ内販売となる)でかつサブスクリプションとなると、割と面倒な対応が必要になります。
ストアの販売用のAPIは新旧の2種類ありますが、Desktop Bridgeのアプリを前提とするならば、新しい
Windows.Services.Store の方を使うことになります。このAPIは
StoreContext オブジェクトのメソッド群で構成され、それなりにサンプルがありますが、肝心な情報の取得方法が抜けているので、その補完がお題です。
1. サブスクリプションのライフサイクル
サブスクリプションの状態によってアドオンの動作を切り替えるだけなら、
が分かれば十分で、詳しいことはウェブ上のMicrosoftアカウントを見てくれ、と割り切るやり方もなくはないです。
ただ、使用期間といっても、サブスクリプションのライフサイクルを考えると、以下のような期間に分かれます(サブスクリプションは自動更新が既定で、現在の期間の使用期限までにキャンセルされない限りは自動更新され、また、キャンセルされた後も現在の期間の使用期限までは使用できる)。
試用期間
試用期間中にキャンセルされた後の使用期限までの期間
試用期間/有償期間後の自動更新により開始された有償期間
有償期間中にキャンセルされた後の使用期限までの期間
サブスクリプション販売の場合、ユーザーの当然の関心事は意図せずに課金されないことだと思います。使用期限前にキャンセルするつもりだったのが、しそこねるといったケースですが、これを避けるにはユーザーが以下の情報を簡単に確認できるようにするのが望ましいです。
現在が(無償)試用期間か有償期間か
現在の期間の使用期限
自動更新が有効になっているか(キャンセルされていないか)否か
これらの正確な情報の取得方法が、このAPIのサンプルでは説明されていません。仕方ないので試行錯誤した結果が以下ですが、これもストアのサーバー次第でいつ変わってもおかしくないので、そのつもりで。
2.1. 使用権があるか否か
これだけなら簡単で、
GetAppLicenseAsync メソッドで可能です。
これで取得できる
StoreAppLicense オブジェクトのうち、アドオンのライセンス情報はAddOnLicensesプロパティの
StoreLicense オブジェクトにあります。ここに目的のアドオンのものがあれば、使用権があるということです。なお、このSkuStoreIdプロパティはStore IDの後にSKU番号が引っ付いたフォーマットで、Store IDそのままではないので要注意。
このメソッドだけは結果がキャッシュされていて、オフラインでも使うことができます。したがって、使用権の確認だけできればいいという場合は、アプリの起動時にこれを実行するだけというシンプルなやり方もあり得ます。
2.2. 現在が試用期間か有償期間か
これには
GetUserCollectionAsync メソッドが使えます。
これで取得できる
StoreProduct オブジェクトに含まれる
StoreCollectionData オブジェクトのIsTrialプロパティが目的のものです。サンプルは以下のとおりです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// using Windows.Services.Store;
public enum LicenseStatus
{
Unknown = 0,
Trial,
Full
}
private static StoreContext _context;
public static async Task CheckSubscriptionAddonStatusesAsync()
{
_context ??= StoreContext.GetDefault();
StoreProductQueryResult queryResult = await _context.GetUserCollectionAsync(new[] { "Durable" });
if (queryResult.ExtendedError != null)
throw queryResult.ExtendedError;
foreach (KeyValuePair<string, StoreProduct> pair in queryResult.Products)
{
StoreSku sku = pair.Value.Skus.FirstOrDefault();
StoreCollectionData data = sku?.CollectionData;
if (data != null)
{
LicenseStatus status = data.IsTrial ? LicenseStatus.Trial : LicenseStatus.Full;
DateTimeOffset startDate = data.StartDate;
bool isAutoRenewal = IsAutoRenewal(data.ExtendedJsonData);
Debug.WriteLine($"Store ID ------- {pair.Key}");
Debug.WriteLine($"License status - {status}");
Debug.WriteLine($"Start date ----- {startDate:yyyy/MM/dd}");
Debug.WriteLine($"Auto renewal --- {isAutoRenewal}");
}
}
}
private static bool IsAutoRenewal(string json)
{
if (string.IsNullOrEmpty(json))
return false;
// For this JSON value, see:
// https://docs.microsoft.com/en-us/windows/uwp/monetize/data-schemas-for-store-products
var pattern = new Regex(@"""autoRenew"":(?<value>true|false)", RegexOptions.IgnoreCase);
var match = pattern.Match(json);
return match.Success
&& bool.TryParse(match.Groups["value"].Value, out bool value)
&& value;
}
この方法に辿り着く前にStack Overflowで質問したのですが、答えを付けたMSFTの人には理解されなかったようで。
このメソッドは経験的には、有償期間に入った後にキャンセルされた後は使用期限前であっても(上記の期間4.のケース)そのStoreProductを返してきません。また、オフラインでは使えないので(実行の度にストアのサーバーと通信するらしい)、必要ならローカルに記録しておく必要があります。
2.3. 現在の期間の使用期限
上記のStoreLicenseにあるExpirationDateプロパティがいかにも使えそうに見えますが、これが曲者で、これ単体では使えません。経験的に整理したところでは、サブスクリプションの自動更新が有効のときは正しい日時から3日後の日時になり、無効のときは大体正しい日時(1日のズレあり)になるようです。
この点からすると、自動更新が有効のときは課金処理の遅延を見越して数字をいじっているのではないかという合理的な疑いがありますが、こういう辻褄合わせのために大元のAPIのデータをいじるのはやってはならないことだと思います。さらに問題なのは、StoreCollectionDataのEndDateプロパティも含め、他のメソッドで取得できる使用期限の日時にも同じ問題があり、どれも信用できないことです。
したがって、自前で計算するしかないわけですが、そのためには、
が必要になってきます。
まず現在の期間の開始日は、StoreCollectionDataのStartDateプロパティが使えます(上記サンプルのとおり)。なお、これにはAcquiredDateプロパティもありますが、これは実際に購入した日時を指すようで、StartDateとは微妙にズレがあります。このズレの法則性はよく分かりませんが、この計算のための開始日としてはStartDateの方が正しいようです。
次に現在の期間の長さは、先に現在が試用期間か有償期間かを判別する必要がありますが、これは上記のとおりStoreCollectionDataのIsTrialプロパティで分かります。期間の長さは、
GetStoreProductsAsync メソッドで取得できるStoreProductオブジェクトで分かります。
なお、この例にある
GetAssociatedStoreProductsAsync メソッドでは有償期間の長さは分かりますが、一旦試用期間に入った後は試用期間の長さは分かりません。というのも、このメソッドが返すのは現在の期間の次に購入可能なSKUだけなので、一旦試用期間に入った後は、次にはもう試用期間はなく、試用期間のSKUは取得できないからです。
この2つが分かれれば、後は
StoreDurationUnit に気を付けて計算するだけです。
なお、上記のとおり期間4.のケースではUserCollectionDataは取得できませんが、このケースでは自動更新が無効となっている結果、StoreLicenseのExpirationDateプロパティが大体正しい日時を示すので、それをそのまま使えばいいでしょう。
[追記]
自分で計算した日時とMicrosoftのサーバーで期限切れまたは自動延長の処理が行われる日時にはズレがあるので(1日以内)、アプリで期限切れに伴う処理を行う前にはこのズレを考慮する必要があります。
2.4. 自動更新が有効になっているか否か
これはどこにも説明はないですが、情報はあります。
このAPIで取得できるオブジェクトにはExtendedJsonDataプロパティがあり、ストアのサーバーから取得したらしきJSONが格納されています。この中にはそのオブジェクトのプロパティに出てこないものもあり、そういうのはこれを直接見て使えということですね。
StoreCollectionDataのJSONはこのスキーマのcollectionDataに相当するようですが、この中にautoRenewの要素があります。これが経験的には自動更新の状態を示していて、これが存在してtrueなら有効、存在しないかfalseなら無効です。上記のサンプルでは、JSON全体をパースする必要もないので、該当部分を正規表現で抽出して判別しています。
3. まとめ
以上のように必要な情報は取得できるようになりましたが、ここに至るまでの試行錯誤に相当な時間を費やさざるを得なかったので、正直どうかと思います。わざわざ新APIを作ってサンプル等もそれなりに用意したにもかかわらず、詰めが甘いというか。結局、ストアのサーバーにある情報をどう引っ張ってくるかという問題なので、一発で必要な情報が揃うようにした方がクエリ数も減っていいと思いますが。
こんな当てにならないAPIに頼るより、購入日をローカルに記録しておけば後は何とでもなるのではないか、と思ったこともありましたが、調べていくにつれそのやり方ではむしろ色々なケースで整合性を保つのが大変すぎると気づいて断念しました。
なお、Desktop Bridgeのアプリの場合でも、Windows Application Packaging Projectを使えばこのAPIのデバッグ実行はUWPと同じようにできるので、それ自体は難しくないです。