2019/05/12

Microsoftストアのサブスクリプション情報の取得

Microsoftストアでは、しばらく前からアプリのアドオンのサブスクリプション販売が可能になっています。アプリ自体を販売する場合は、アプリ側の対応はそれほど手間ではないですが、アドオン(アプリ内販売となる)でかつサブスクリプションとなると、割と面倒な対応が必要になります。

ストアの販売用のAPIは新旧の2種類ありますが、Desktop Bridgeのアプリを前提とすると、新しいWindows.Services.Storeの方を使うことになります。このAPIはStoreContextオブジェクトのメソッド群で構成され、それなりにサンプルがありますが、肝心な情報の取得方法が抜けているので、その補完がお題です。
1. サブスクリプションのライフサイクル

サブスクリプションの状態によってアドオンの動作を切り替えるだけなら、
  • 使用権があるか否か
が分かれば十分で、詳しいことはウェブ上のMicrosoftアカウントを見てくれ、と割り切るやり方もなくはないです。

ただ、使用期間といっても、サブスクリプションのライフサイクルを考えると、以下のような期間に分かれます(サブスクリプションは自動更新が既定で、現在の期間の使用期限までにキャンセルされない限りは自動更新され、また、キャンセルされた後も現在の期間の使用期限までは使用できる)。
  1. 試用期間
  2. 試用期間中にキャンセルされた後の使用期限までの期間
  3. 試用期間/有償期間後の自動更新により開始された有償期間
  4. 有償期間中にキャンセルされた後の使用期限までの期間
サブスクリプション販売の場合、ユーザーの当然の関心事は意図せずに課金されないことだと思います。使用期限前にキャンセルするつもりだったのが、しそこねるといったケースですが、これを避けるにはユーザーが以下の情報を簡単に確認できるようにするのが望ましいです。
  • 現在が(無償)試用期間か有償期間か
  • 現在の期間の使用期限
  • 自動更新が有効になっているか(キャンセルされていないか)否か
これらの正確な情報の取得方法が、このAPIのサンプルでは説明されていません。仕方ないので試行錯誤した結果が以下ですが、これもストアのサーバー次第でいつ変わってもおかしくないので、そのつもりで。

2.1. 使用権があるか否か

これだけなら簡単で、GetAppLicenseAsyncメソッドで可能です。
これで取得できるStoreAppLicenseオブジェクトのうち、アドオンのライセンス情報はAddOnLicensesプロパティのStoreLicenseオブジェクトにあります。ここに目的のアドオンのものがあれば、使用権があるということです。なお、このSkuStoreIdプロパティはStore IDの後にSKU番号が引っ付いたフォーマットで、Store IDそのままではないので要注意。

このメソッドだけは結果がキャッシュされていて、オフラインでも使うことができます。したがって、使用権の確認だけできればいいという場合は、アプリの起動時にこれを実行するだけというシンプルなやり方もあり得ます。

2.2. 現在が試用期間か有償期間か

これにはGetUserCollectionAsyncメソッドが使えます。
これで取得できるStoreProductオブジェクトに含まれるStoreCollectionDataオブジェクトのIsTrialプロパティが目的のものです。サンプルは以下のとおりです。

この方法に辿り着く前に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プロパティが大体正しい日時を示すので、それをそのまま使えばいいでしょう。

2.3. 自動更新が有効になっているか否か

これはどこにも説明はないですが、情報はあります。

このAPIで取得できるオブジェクトにはExtendedJsonDataプロパティがあり、ストアのサーバーから取得したらしきJSONが格納されています。この中にはそのオブジェクトのプロパティに出てこないものもあり、そういうのはこれを直接見て使えということですね。
StoreCollectionDataのJSONはこのスキーマのcollectionDataに相当するようですが、この中にautoRenewの要素があります。これが経験的には自動更新の状態を示していて、これが存在してtrueなら有効、存在しないかfalseなら無効です。上記のサンプルでは、JSON全体をパースする必要もないので、該当部分を正規表現で抽出して判別しています。

3. まとめ

以上のように必要な情報は取得できますが、ここに至るまでの試行錯誤に相当な時間を費やさざるを得なかったので、正直色々どうかと思います。わざわざ新APIを作ってサンプル等もそれなりに用意したにもかかわらず、詰めが甘いというか。結局、ストアのサーバーにある情報をどう引っ張ってくるかという問題なので、一発で必要な情報が揃うようにした方がクエリ数も減っていいと思いますが。

なお、Desktop Bridgeのアプリの場合でも、Windows Application Packaging Projectを使えばこのAPIのデバッグ実行はUWPと同じようにできるので、それ自体は難しくないです。
[追記]

こんな当てにならないAPIに頼るより、購入日をローカルに記録しておけば後は何とでもなるのではないか、と思ったときもありましたが、調べていくにつれそのやり方ではむしろ整合性を保つのが大変すぎると気づいて断念しました。

2018/11/25

MicrosoftストアとPrivacy Policy

先月、Microsoftストアの管理用のダッシュボードで、Monitorianの更新のサブミッションを出そうとしたとき、Propertiesの項が前回から変えていないのにIncompleteになって、サブミッションの提出ができないことがあった。

何かと思ってPropertiesの項を開いてみると、個人情報の取扱いについてNoを選択していたら、そのせいで設定が完了していないことになっていた。

大前提としてMonitorianは個人情報に触らないので、ここは最初のサブミッションからNoを選択してきた。前回のサブミッションが通過したのが10月10日、この時が10月17日だったので、この間に何らかの変更があったのか、その前の変更の影響がこのタイミングで現れたのか、その辺は不明。

いずれにせよ、Privacy policy URLの入力が必要なのはYesを選択したときだけのはずで、ダッシュボードの動作としておかしいので、インシデントとして報告した。

過去の他の経験から、まず問題をサポートに理解してもらうのが手間かなと思っていたら、サポートの反応は迅速で、かつ、一発で問題を正確に理解していたので少し驚いた。前回と変えていないのに引っ掛かったことについて調査してもらった結果、以下のことが判明。
  • アプリのCapabilityとしてrunFullTrustを宣言しているので、このCapabilityの場合は、アプリの実際の動作にかかわらず、Privacy policyが必要。
  • 前回までこの問題が出なかった理由は不明だが、いずれにせよ現在は必要。
このrunFullTrust(Full Trust Permission Level)はDesktop Bridgeを利用する場合にはそう指定することになっているので、開発者に選択肢はないが、確かにかなり強いCapabilityなので、Privacy policyを要求するのは分からなくもない。というか、その可能性はあるだろうと思ってはいた。一方、弱いCapabilityでも個人情報を扱うことはあり得るので、本質的には別次元の問題。

ともあれ、このアプリは個人情報を取得しないという一文であっても、Microsoftストアのシステム上必要なのであれば書くが、Yesを選択すると事実と異なる虚偽の記載となって、後でペナルティ等はないのかと質問したところ(しつこく聞こえるかもしれないが、虚偽の記載は何であれ後で不利に利用されかねない)、以下の回答。
  • ここでYesを選択することによって、ペナルティを課されることはない。
ということで、確認はしたので、Privacy policyを作成してサブミッションを通過させた。

以上、デスクトップアプリをMicrosoftストアに出すにはDesktop Bridgeが必要、Desktop BridgeはrunFullTrustが必要、MicrosoftストアはrunFullTrustだとPrivacy policyが必要、よって、導き出される結論は、デスクトップアプリをMicrosoftストアに出すにはPrivacy policyが必要ということになる。

その後、ダッシュボードは修正されて、11月25日現在、以下のようになっている(Wifinianのもの)。

CapabilitiesとPrivacy policyの関係が明示されたので、その点は分かりやすくなった。