Как и многие другие приложения, UniShare недавно пострадала из-за сбоя в системе лицензирования Windows Phone Store.
Был обходной путь, чтобы вернуть лицензии из магазина, но даже при этом пользователи оставили отрицательные голоса (даже после того, как я опубликовал обходной путь, а также wpcentral написал об этом несколько дней спустя ).
Проблема с этими отрицательными отзывами: они, как правило, остаются даже после ответа на них (если это возможно) или общения с пользователями по почте. Я полностью понимаю пользователей, которые раздражены такими фактами — меня также поразили другие приложения, которыми я регулярно пользуюсь. Поэтому я подумал о лучшем решении, рекомендованном Microsoft, который говорит, что вы должны проверять его при каждом запуске приложения.
Роб Ирвинг написал о своем решении в тот же день, когда wpcentral написал об этом , что является одним из возможных решений. Его мотив был таким же, как и мое решение — улучшение взаимодействия с пользователями IAP.
Однако я предпочитаю время от времени проверять лицензии в Магазине, чтобы убедиться, что лицензии все еще верны.
Вот мое решение (для длительного IAP):
First, let’s we need to declare an object for the ListingInformation, which will hold the information that the store returns for our IAP:
ListingInformation IAPListing;
Then, we need to create these two classes:
public class IAP { public string Key { get; set; } public string Name { get; set; } public string Price { get; set; } public ProductType Type { get; set; } public string Description { get; set; } public string Image { get; set; } public bool IsLicenseActive { get; set; } } public class IAPToSave { public List<IAP> IAPListToSave { get; set; } public DateTime date { get; set; } }
The class IAP is the class/model that holds a single IAP item information. The second class is needed for saving the fetched IAP information (we’ll see later how I did it).
Now we have prepared these, we can finally go to the store and fetch the IAP list. I created an async Task that returns a List<IAP> for it:
public async Task<List<IAP>> GetAllIAP() { var list = new List<IAP>(); IAPListing = await CurrentApp.LoadListingInformationAsync(); foreach (var product in IAPListing.ProductListings) { list.Add(new IAP() { Key = product.Key, Name = product.Value.Name, Description = product.Value.Description, Price = product.Value.FormattedPrice, Type = product.Value.ProductType, Image = product.Value.ImageUri.ToString(), IsLicenseActive = isPackageUnlocked(product.Key) }); } return list; }
To immediately check if our user has already purchased the item, we are getting a Boolean for it. While we are getting the data from the store, we are using this to fill our IAP class with the desired value (see IsLicenseActive property in the IAP item above).
public bool isPackageUnlocked(string productKey) { var licenseInformation = CurrentApp.LicenseInformation; if (licenseInformation.ProductLicenses[productKey].IsActive) { return true; } else { return false; } }
Now we have already all data that we need to display all IAP in our app. The usage is fairly simple:
var iapHelper = new IAPHelper(); var iapList = await iapHelper.GetAllIAP();
You can now bind the iapList to a ListBox (or your proper control/view). Next thing we are creating is our helper that performs the purchase action and returns a result string to display in a message to the user in our IAPHelper class:
public async Task<string> unlockPackage(string productKey, string productName) { var licenseInformation = CurrentApp.LicenseInformation; string response = string.Empty; if (!licenseInformation.ProductLicenses[productKey].IsActive) { try { //opening the store to display the purchase page await CurrentApp.RequestProductPurchaseAsync(productKey); //getting the result after returning into app var isUnlocked = isPackageUnlocked(productKey); if (isUnlocked == true) { response = string.Format("You succesfully unlocked {0}.", productName); } else { response = string.Format("There was an error while trying to unlock {0}. Please try again.", productName); } } catch (Exception) { response = string.Format("There was an error while trying to unlock {0}. Please try again.", productName); } } else { response = string.Format( "You already unlocked {0}", productName); } return response; }
You may of course vary the messages that are displayed to the user to your favor.
The usage of this Task is also pretty straight forward:
var iapHelper = new IAPHelper(); string message = await iapHelper.unlockPackage(((IAPHelper.IAP)IAPListBox.SelectedItem).Key, ((IAPHelper.IAP)IAPListBox.SelectedItem).Name);
After that, we need to refresh the LicenseInformation by using GetAllIAP() again to refresh the iapList and of course our ListBox.
My goal was to save the LicenseInformation of my IAP for a limited time so the user is protected for future outages (or situations where no network connection is available). That’s why we need to add another Task to our IAPHelper class:
public async Task<string> SerializedCurrentIAPList(List<IAP> iaplist, DateTime lastchecked) { string json = string.Empty; IAPToSave listToSave = new IAPToSave() { IAPListToSave = iaplist, date = lastchecked }; if (iaplist.Count != 0) { json = await Task.Factory.StartNew(() => JsonConvert.SerializeObject(listToSave)); } return json; }
As you can see, now is the point where we need the second class I mentioned in the beginning. It has one property for the List<IAP> and a DateTime property that we are saving. I am serializing the whole class to a JSON string. This way, we are able to save it as a string in our application’s storage.
The usage of this Task is as simple as the former ones:
var savedIAPList = await iapHelper.SerializedCurrentIAPList(iapList, DateTime.Now);
The last thing we need to create is an object that helps us indicating if a refresh of the list is needed or not. Like I said, I want to do this action time based, so here is my way to get this value:
public bool isReloadNeeded(DateTime lastchecked, TimeSpan desiredtimespan) { bool reloadNeeded = false; var now = DateTime.Now; TimeSpan ts_lastchecked = now - lastchecked; if (ts_lastchecked > desiredtimespan) { reloadNeeded = true; } return reloadNeeded; }
This method just checks if the desired TimeSpan has passed already and returns the matching Boolean value. The usage of this is likewise pretty simple:
var savedlist = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject<IAPHelper.IAPToSave>(App.SettingsStore.savedIAPList)); if (iapHelper.isReloadNeeded(savedlist.date, new TimeSpan(96, 0, 0))) { //reload the list and perform your actions } else { //use savedlist.IAPListToSave and perform your actions }
This way, we are able to make sure that all IAPs are available for a minimum of time and protect our users against store outages.
For your convenience, you can download the whole class right here. Just replace NAMESPACE with yours and you are good to go.
Note: I know that this approach does not follow the recommended way of Microsoft. It is up to us to deal with bad reviews if something on the store part is not working. This is my approach to avoid negative reviews because of store outages (at least for a limited time). However, like always, I hope this post is helpful for some of you.
Happy coding!