using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Security;
namespace UnityEngine.Purchasing
{
///
/// A period of time expressed in either days, months, or years. Conveys a subscription's duration definition.
/// Note this reflects the types of subscription durations settable on a subscription on supported app stores.
///
public class TimeSpanUnits
{
///
/// Discrete duration in days, if less than a month, otherwise zero.
///
public double days;
///
/// Discrete duration in months, if less than a year, otherwise zero.
///
public int months;
///
/// Discrete duration in years, otherwise zero.
///
public int years;
///
/// Construct a subscription duration.
///
/// Discrete duration in days, if less than a month, otherwise zero.
/// Discrete duration in months, if less than a year, otherwise zero.
/// Discrete duration in years, otherwise zero.
public TimeSpanUnits(double d, int m, int y)
{
days = d;
months = m;
years = y;
}
}
///
/// Use to query in-app purchasing subscription product information, and upgrade subscription products.
/// Supports the Apple App Store, Google Play store, and Amazon AppStore.
/// Note Amazon support offers no subscription duration information.
/// Note expiration dates may become invalid after updating subscriptions between two types of duration.
///
///
///
public class SubscriptionManager
{
private readonly string receipt;
private readonly string productId;
private readonly string intro_json;
///
/// Performs subscription updating, migrating a subscription into another as long as they are both members
/// of the same subscription group on the App Store.
///
/// Destination subscription product, belonging to the same subscription group as
/// Source subscription product, belonging to the same subscription group as
/// Carried-over metadata from prior call to
/// Triggered upon completion of the subscription update.
/// Triggered upon completion of the subscription update.
public static void UpdateSubscription(Product newProduct, Product oldProduct, string developerPayload, Action appleStore, Action googleStore)
{
if (oldProduct.receipt == null)
{
Debug.LogError("The product has not been purchased, a subscription can only be upgrade/downgrade when has already been purchased");
return;
}
var receipt_wrapper = (Dictionary)MiniJson.JsonDecode(oldProduct.receipt);
if (receipt_wrapper == null || !receipt_wrapper.ContainsKey("Store") || !receipt_wrapper.ContainsKey("Payload"))
{
Debug.LogWarning("The product receipt does not contain enough information");
return;
}
var store = (string)receipt_wrapper["Store"];
var payload = (string)receipt_wrapper["Payload"];
if (payload != null)
{
switch (store)
{
case "GooglePlay":
{
var oldSubscriptionManager = new SubscriptionManager(oldProduct, null);
SubscriptionInfo oldSubscriptionInfo;
try
{
oldSubscriptionInfo = oldSubscriptionManager.getSubscriptionInfo();
}
catch (Exception e)
{
Debug.unityLogger.LogError("Error: the product that will be updated does not have a valid receipt", e);
return;
}
var newSubscriptionId = newProduct.definition.storeSpecificId;
googleStore(oldSubscriptionInfo.getSubscriptionInfoJsonString(), newSubscriptionId);
return;
}
case "AppleAppStore":
case "MacAppStore":
{
appleStore(newProduct, developerPayload);
return;
}
default:
{
Debug.LogWarning("This store does not support update subscriptions");
return;
}
}
}
}
///
/// Performs subscription updating, migrating a subscription into another as long as they are both members
/// of the same subscription group on the App Store.
///
/// Source subscription product, belonging to the same subscription group as
/// Destination subscription product, belonging to the same subscription group as
/// Triggered upon completion of the subscription update.
public static void UpdateSubscriptionInGooglePlayStore(Product oldProduct, Product newProduct, Action googlePlayUpdateCallback)
{
var oldSubscriptionManager = new SubscriptionManager(oldProduct, null);
SubscriptionInfo oldSubscriptionInfo;
try
{
oldSubscriptionInfo = oldSubscriptionManager.getSubscriptionInfo();
}
catch (Exception e)
{
Debug.unityLogger.LogError("Error: the product that will be updated does not have a valid receipt", e);
return;
}
var newSubscriptionId = newProduct.definition.storeSpecificId;
googlePlayUpdateCallback(oldSubscriptionInfo.getSubscriptionInfoJsonString(), newSubscriptionId);
}
///
/// Performs subscription updating, migrating a subscription into another as long as they are both members
/// of the same subscription group on the App Store.
///
/// Destination subscription product, belonging to the same subscription group as
/// Carried-over metadata from prior call to
/// Triggered upon completion of the subscription update.
public static void UpdateSubscriptionInAppleStore(Product newProduct, string developerPayload, Action appleStoreUpdateCallback)
{
appleStoreUpdateCallback(newProduct, developerPayload);
}
///
/// Construct an object that allows inspection of a subscription product.
///
/// Subscription to be inspected
/// From
public SubscriptionManager(Product product, string intro_json)
{
receipt = product.receipt;
productId = product.definition.storeSpecificId;
this.intro_json = intro_json;
}
///
/// Construct an object that allows inspection of a subscription product.
///
/// A Unity IAP unified receipt from
/// A product identifier.
/// From
public SubscriptionManager(string receipt, string id, string intro_json)
{
this.receipt = receipt;
productId = id;
this.intro_json = intro_json;
}
///
/// Convert my Product into a .
/// My Product.receipt must have a "Payload" JSON key containing supported native app store
/// information, which will be converted here.
///
///
/// My Product must have a non-null product identifier
/// A supported app store must be used as my product
/// My product must have
public SubscriptionInfo getSubscriptionInfo()
{
if (receipt != null)
{
var receipt_wrapper = (Dictionary)MiniJson.JsonDecode(receipt);
var validPayload = receipt_wrapper.TryGetValue("Payload", out var payloadAsObject);
var validStore = receipt_wrapper.TryGetValue("Store", out var storeAsObject);
if (validPayload && validStore)
{
var payload = payloadAsObject as string;
var store = storeAsObject as string;
switch (store)
{
case GooglePlay.Name:
{
return getGooglePlayStoreSubInfo(payload);
}
case AppleAppStore.Name:
case MacAppStore.Name:
{
if (productId == null)
{
throw new NullProductIdException();
}
return getAppleAppStoreSubInfo(payload, productId);
}
case AmazonApps.Name:
{
return getAmazonAppStoreSubInfo(productId);
}
default:
{
throw new StoreSubscriptionInfoNotSupportedException("Store not supported: " + store);
}
}
}
}
throw new NullReceiptException();
}
private SubscriptionInfo getAmazonAppStoreSubInfo(string productId)
{
return new SubscriptionInfo(productId);
}
private SubscriptionInfo getAppleAppStoreSubInfo(string payload, string productId)
{
AppleReceipt receipt = null;
var logger = Debug.unityLogger;
try
{
receipt = new AppleReceiptParser().Parse(Convert.FromBase64String(payload));
}
catch (ArgumentException e)
{
logger.Log("Unable to parse Apple receipt", e);
}
catch (IAPSecurityException e)
{
logger.Log("Unable to parse Apple receipt", e);
}
catch (NullReferenceException e)
{
logger.Log("Unable to parse Apple receipt", e);
}
var inAppPurchaseReceipts = new List();
if (receipt != null && receipt.inAppPurchaseReceipts != null && receipt.inAppPurchaseReceipts.Length > 0)
{
foreach (var r in receipt.inAppPurchaseReceipts)
{
if (r.productID.Equals(productId))
{
inAppPurchaseReceipts.Add(r);
}
}
}
return inAppPurchaseReceipts.Count == 0 ? null : new SubscriptionInfo(findMostRecentReceipt(inAppPurchaseReceipts), intro_json);
}
private AppleInAppPurchaseReceipt findMostRecentReceipt(List receipts)
{
receipts.Sort((b, a) => a.purchaseDate.CompareTo(b.purchaseDate));
return receipts[0];
}
private SubscriptionInfo getGooglePlayStoreSubInfo(string payload)
{
var payload_wrapper = (Dictionary)MiniJson.JsonDecode(payload);
payload_wrapper.TryGetValue("skuDetails", out var skuDetailsObject);
var skuDetails = (skuDetailsObject as List)?.Select(obj => obj as string);
var purchaseHistorySupported = false;
var original_json_payload_wrapper =
(Dictionary)MiniJson.JsonDecode((string)payload_wrapper["json"]);
var validIsAutoRenewingKey =
original_json_payload_wrapper.TryGetValue("autoRenewing", out var autoRenewingObject);
var isAutoRenewing = false;
if (validIsAutoRenewingKey)
{
isAutoRenewing = (bool)autoRenewingObject;
}
// Google specifies times in milliseconds since 1970.
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var validPurchaseTimeKey =
original_json_payload_wrapper.TryGetValue("purchaseTime", out var purchaseTimeObject);
long purchaseTime = 0;
if (validPurchaseTimeKey)
{
purchaseTime = (long)purchaseTimeObject;
}
var purchaseDate = epoch.AddMilliseconds(purchaseTime);
var validDeveloperPayloadKey =
original_json_payload_wrapper.TryGetValue("developerPayload", out var developerPayloadObject);
var isFreeTrial = false;
var hasIntroductoryPrice = false;
string updateMetadata = null;
if (validDeveloperPayloadKey)
{
var developerPayloadJSON = (string)developerPayloadObject;
var developerPayload_wrapper = (Dictionary)MiniJson.JsonDecode(developerPayloadJSON);
var validIsFreeTrialKey =
developerPayload_wrapper.TryGetValue("is_free_trial", out var isFreeTrialObject);
if (validIsFreeTrialKey)
{
isFreeTrial = (bool)isFreeTrialObject;
}
var validHasIntroductoryPriceKey =
developerPayload_wrapper.TryGetValue("has_introductory_price_trial",
out var hasIntroductoryPriceObject);
if (validHasIntroductoryPriceKey)
{
hasIntroductoryPrice = (bool)hasIntroductoryPriceObject;
}
var validIsUpdatedKey = developerPayload_wrapper.TryGetValue("is_updated", out var isUpdatedObject);
var isUpdated = false;
if (validIsUpdatedKey)
{
isUpdated = (bool)isUpdatedObject;
}
if (isUpdated)
{
var isValidUpdateMetaKey = developerPayload_wrapper.TryGetValue("update_subscription_metadata",
out var updateMetadataObject);
if (isValidUpdateMetaKey)
{
updateMetadata = (string)updateMetadataObject;
}
}
}
var skuDetail = skuDetails.First();
return new SubscriptionInfo(skuDetail, isAutoRenewing, purchaseDate, isFreeTrial, hasIntroductoryPrice,
purchaseHistorySupported, updateMetadata);
}
}
///
/// A container for a Product’s subscription-related information.
///
///
public class SubscriptionInfo
{
private readonly Result is_subscribed;
private readonly Result is_expired;
private readonly Result is_cancelled;
private readonly Result is_free_trial;
private readonly Result is_auto_renewing;
private readonly Result is_introductory_price_period;
private readonly string productId;
private readonly DateTime purchaseDate;
private readonly DateTime subscriptionExpireDate;
private readonly DateTime subscriptionCancelDate;
private readonly TimeSpan remainedTime;
private readonly string introductory_price;
private readonly TimeSpan introductory_price_period;
private readonly long introductory_price_cycles;
private readonly TimeSpan freeTrialPeriod;
private readonly TimeSpan subscriptionPeriod;
// for test
private readonly string free_trial_period_string;
private readonly string sku_details;
///
/// Unpack Apple receipt subscription data.
///
/// The Apple receipt from
/// From . Keys:
/// introductoryPriceLocale , introductoryPrice , introductoryPriceNumberOfPeriods , numberOfUnits ,
/// unit , which can be fetched from Apple's remote service.
/// Error found involving an invalid product type.
///
public SubscriptionInfo(AppleInAppPurchaseReceipt r, string intro_json)
{
var productType = (AppleStoreProductType)Enum.Parse(typeof(AppleStoreProductType), r.productType.ToString());
if (productType == AppleStoreProductType.Consumable || productType == AppleStoreProductType.NonConsumable)
{
throw new InvalidProductTypeException();
}
if (!string.IsNullOrEmpty(intro_json))
{
var intro_wrapper = (Dictionary)MiniJson.JsonDecode(intro_json);
var nunit = -1;
var unit = SubscriptionPeriodUnit.NotAvailable;
introductory_price = intro_wrapper.TryGetString("introductoryPrice") + intro_wrapper.TryGetString("introductoryPriceLocale");
if (string.IsNullOrEmpty(introductory_price))
{
introductory_price = "not available";
}
else
{
try
{
introductory_price_cycles = Convert.ToInt64(intro_wrapper.TryGetString("introductoryPriceNumberOfPeriods"));
nunit = Convert.ToInt32(intro_wrapper.TryGetString("numberOfUnits"));
unit = (SubscriptionPeriodUnit)Convert.ToInt32(intro_wrapper.TryGetString("unit"));
}
catch (Exception e)
{
Debug.unityLogger.Log("Unable to parse introductory period cycles and duration, this product does not have configuration of introductory price period", e);
unit = SubscriptionPeriodUnit.NotAvailable;
}
}
var now = DateTime.Now;
switch (unit)
{
case SubscriptionPeriodUnit.Day:
introductory_price_period = TimeSpan.FromTicks(TimeSpan.FromDays(1).Ticks * nunit);
break;
case SubscriptionPeriodUnit.Month:
var month_span = now.AddMonths(1) - now;
introductory_price_period = TimeSpan.FromTicks(month_span.Ticks * nunit);
break;
case SubscriptionPeriodUnit.Week:
introductory_price_period = TimeSpan.FromTicks(TimeSpan.FromDays(7).Ticks * nunit);
break;
case SubscriptionPeriodUnit.Year:
var year_span = now.AddYears(1) - now;
introductory_price_period = TimeSpan.FromTicks(year_span.Ticks * nunit);
break;
case SubscriptionPeriodUnit.NotAvailable:
introductory_price_period = TimeSpan.Zero;
introductory_price_cycles = 0;
break;
}
}
else
{
introductory_price = "not available";
introductory_price_period = TimeSpan.Zero;
introductory_price_cycles = 0;
}
var current_date = DateTime.UtcNow;
purchaseDate = r.purchaseDate;
productId = r.productID;
subscriptionExpireDate = r.subscriptionExpirationDate;
subscriptionCancelDate = r.cancellationDate;
// if the product is non-renewing subscription, apple store will not return expiration date for this product
if (productType == AppleStoreProductType.NonRenewingSubscription)
{
is_subscribed = Result.Unsupported;
is_expired = Result.Unsupported;
is_cancelled = Result.Unsupported;
is_free_trial = Result.Unsupported;
is_auto_renewing = Result.Unsupported;
is_introductory_price_period = Result.Unsupported;
}
else
{
is_cancelled = (r.cancellationDate.Ticks > 0) && (r.cancellationDate.Ticks < current_date.Ticks) ? Result.True : Result.False;
is_subscribed = r.subscriptionExpirationDate.Ticks >= current_date.Ticks ? Result.True : Result.False;
is_expired = (r.subscriptionExpirationDate.Ticks > 0 && r.subscriptionExpirationDate.Ticks < current_date.Ticks) ? Result.True : Result.False;
is_free_trial = (r.isFreeTrial == 1) ? Result.True : Result.False;
is_auto_renewing = ((productType == AppleStoreProductType.AutoRenewingSubscription) && is_cancelled == Result.False
&& is_expired == Result.False) ? Result.True : Result.False;
is_introductory_price_period = r.isIntroductoryPricePeriod == 1 ? Result.True : Result.False;
}
remainedTime = is_subscribed == Result.True ? r.subscriptionExpirationDate.Subtract(current_date) : TimeSpan.Zero;
}
///
/// Especially crucial values relating to Google subscription products.
/// Note this is intended to be called internally.
///
/// The raw JSON from SkuDetail.getOriginalJson
/// Whether this subscription is expected to auto-renew
/// A date this subscription was billed
/// Indicates whether this Product is a free trial
/// Indicates whether this Product may be owned with an introductory price period.
/// Unsupported
/// Unsupported. Mechanism previously propagated subscription upgrade information to new subscription.
/// For non-subscription product types.
public SubscriptionInfo(string skuDetails, bool isAutoRenewing, DateTime purchaseDate, bool isFreeTrial,
bool hasIntroductoryPriceTrial, bool purchaseHistorySupported, string updateMetadata)
{
var skuDetails_wrapper = (Dictionary)MiniJson.JsonDecode(skuDetails);
var validTypeKey = skuDetails_wrapper.TryGetValue("type", out var typeObject);
if (!validTypeKey || (string)typeObject == "inapp")
{
throw new InvalidProductTypeException();
}
var validProductIdKey = skuDetails_wrapper.TryGetValue("productId", out var productIdObject);
productId = null;
if (validProductIdKey)
{
productId = productIdObject as string;
}
this.purchaseDate = purchaseDate;
is_subscribed = Result.True;
is_auto_renewing = isAutoRenewing ? Result.True : Result.False;
is_expired = Result.False;
is_cancelled = isAutoRenewing ? Result.False : Result.True;
is_free_trial = Result.False;
string sub_period = null;
if (skuDetails_wrapper.ContainsKey("subscriptionPeriod"))
{
sub_period = (string)skuDetails_wrapper["subscriptionPeriod"];
}
string free_trial_period = null;
if (skuDetails_wrapper.ContainsKey("freeTrialPeriod"))
{
free_trial_period = (string)skuDetails_wrapper["freeTrialPeriod"];
}
string introductory_price = null;
if (skuDetails_wrapper.ContainsKey("introductoryPrice"))
{
introductory_price = (string)skuDetails_wrapper["introductoryPrice"];
}
string introductory_price_period_string = null;
if (skuDetails_wrapper.ContainsKey("introductoryPricePeriod"))
{
introductory_price_period_string = (string)skuDetails_wrapper["introductoryPricePeriod"];
}
long introductory_price_cycles = 0;
if (skuDetails_wrapper.ContainsKey("introductoryPriceCycles"))
{
introductory_price_cycles = (long)skuDetails_wrapper["introductoryPriceCycles"];
}
// for test
free_trial_period_string = free_trial_period;
subscriptionPeriod = computePeriodTimeSpan(parsePeriodTimeSpanUnits(sub_period));
freeTrialPeriod = TimeSpan.Zero;
if (isFreeTrial)
{
freeTrialPeriod = parseTimeSpan(free_trial_period);
}
this.introductory_price = introductory_price;
this.introductory_price_cycles = introductory_price_cycles;
introductory_price_period = TimeSpan.Zero;
is_introductory_price_period = Result.False;
var total_introductory_duration = TimeSpan.Zero;
if (hasIntroductoryPriceTrial)
{
introductory_price_period = introductory_price_period_string != null && introductory_price_period_string.Equals(sub_period)
? subscriptionPeriod
: parseTimeSpan(introductory_price_period_string);
// compute the total introductory duration according to the introductory price period and period cycles
total_introductory_duration = accumulateIntroductoryDuration(parsePeriodTimeSpanUnits(introductory_price_period_string), this.introductory_price_cycles);
}
// if this subscription is updated from other subscription, the remaining time will be applied to this subscription
var extra_time = TimeSpan.FromSeconds(updateMetadata == null ? 0.0 : computeExtraTime(updateMetadata, subscriptionPeriod.TotalSeconds));
var time_since_purchased = DateTime.UtcNow.Subtract(purchaseDate);
// this subscription is still in the extra time (the time left by the previous subscription when updated to the current one)
if (time_since_purchased <= extra_time)
{
// this subscription is in the remaining credits from the previous updated one
subscriptionExpireDate = purchaseDate.Add(extra_time);
}
else if (time_since_purchased <= freeTrialPeriod.Add(extra_time))
{
// this subscription is in the free trial period
// this product will be valid until free trial ends, the beginning of next billing date
is_free_trial = Result.True;
subscriptionExpireDate = purchaseDate.Add(freeTrialPeriod.Add(extra_time));
}
else if (time_since_purchased < freeTrialPeriod.Add(extra_time).Add(total_introductory_duration))
{
// this subscription is in the introductory price period
is_introductory_price_period = Result.True;
var introductory_price_begin_date = this.purchaseDate.Add(freeTrialPeriod.Add(extra_time));
subscriptionExpireDate = nextBillingDate(introductory_price_begin_date, parsePeriodTimeSpanUnits(introductory_price_period_string));
}
else
{
// no matter sub is cancelled or not, the expire date will be next billing date
var billing_begin_date = this.purchaseDate.Add(freeTrialPeriod.Add(extra_time).Add(total_introductory_duration));
subscriptionExpireDate = nextBillingDate(billing_begin_date, parsePeriodTimeSpanUnits(sub_period));
}
remainedTime = subscriptionExpireDate.Subtract(DateTime.UtcNow);
sku_details = skuDetails;
}
///
/// Especially crucial values relating to subscription products.
/// Note this is intended to be called internally.
///
/// This subscription's product identifier
public SubscriptionInfo(string productId)
{
this.productId = productId;
is_subscribed = Result.True;
is_expired = Result.False;
is_cancelled = Result.Unsupported;
is_free_trial = Result.Unsupported;
is_auto_renewing = Result.Unsupported;
remainedTime = TimeSpan.MaxValue;
is_introductory_price_period = Result.Unsupported;
introductory_price_period = TimeSpan.MaxValue;
introductory_price = null;
introductory_price_cycles = 0;
}
///
/// Store specific product identifier.
///
/// The product identifier from the store receipt.
public string getProductId() { return productId; }
///
/// A date this subscription was billed.
/// Note the store-specific behavior.
///
///
/// For Apple, the purchase date is the date when the subscription was either purchased or renewed.
/// For Google, the purchase date is the date when the subscription was originally purchased.
///
public DateTime getPurchaseDate() { return purchaseDate; }
///
/// Indicates whether this auto-renewable subscription Product is currently subscribed or not.
/// Note the store-specific behavior.
/// Note also that the receipt may update and change this subscription expiration status if the user sends
/// their iOS app to the background and then returns it to the foreground. It is therefore recommended to remember
/// subscription expiration state at app-launch, and ignore the fact that a subscription may expire later during
/// this app launch runtime session.
///
///
/// Subscription status if the store receipt's expiration date is
/// after the device's current time.
/// otherwise.
/// Non-renewable subscriptions in the Apple store return a value.
///
///
///
public Result isSubscribed() { return is_subscribed; }
///
/// Indicates whether this auto-renewable subscription Product is currently unsubscribed or not.
/// Note the store-specific behavior.
/// Note also that the receipt may update and change this subscription expiration status if the user sends
/// their iOS app to the background and then returns it to the foreground. It is therefore recommended to remember
/// subscription expiration state at app-launch, and ignore the fact that a subscription may expire later during
/// this app launch runtime session.
///
///
/// Subscription status if the store receipt's expiration date is
/// before the device's current time.
/// otherwise.
/// Non-renewable subscriptions in the Apple store return a value.
///
///
///
public Result isExpired() { return is_expired; }
///
/// Indicates whether this Product has been cancelled.
/// A cancelled subscription means the Product is currently subscribed, and will not renew on the next billing date.
///
///
/// Cancellation status if the store receipt's indicates this subscription is cancelled.
/// otherwise.
/// Non-renewable subscriptions in the Apple store return a value.
///
public Result isCancelled() { return is_cancelled; }
///
/// Indicates whether this Product is a free trial.
/// Note the store-specific behavior.
///
///
/// This subscription is a free trial according to the store receipt.
/// This subscription is not a free trial according to the store receipt.
/// Non-renewable subscriptions in the Apple store
/// and Google subscriptions queried on devices with version lower than 6 of the Android in-app billing API return a value.
///
public Result isFreeTrial() { return is_free_trial; }
///
/// Indicates whether this Product is expected to auto-renew. The product must be auto-renewable, not canceled, and not expired.
///
///
/// The store receipt's indicates this subscription is auto-renewing.
/// The store receipt's indicates this subscription is not auto-renewing.
/// Non-renewable subscriptions in the Apple store return a value.
///
public Result isAutoRenewing() { return is_auto_renewing; }
///
/// Indicates how much time remains until the next billing date.
/// Note the store-specific behavior.
/// Note also that the receipt may update and change this subscription expiration status if the user sends
/// their iOS app to the background and then returns it to the foreground.
///
///
/// A time duration from now until subscription billing occurs.
/// Google subscriptions queried on devices with version lower than 6 of the Android in-app billing API return .
///
///
public TimeSpan getRemainingTime() { return remainedTime; }
///
/// Indicates whether this Product is currently owned within an introductory price period.
/// Note the store-specific behavior.
///
///
/// The store receipt's indicates this subscription is within its introductory price period.
/// The store receipt's indicates this subscription is not within its introductory price period.
/// If the product is not configured to have an introductory period.
/// Non-renewable subscriptions in the Apple store return a value.
/// Google subscriptions queried on devices with version lower than 6 of the Android in-app billing API return a value.
///
public Result isIntroductoryPricePeriod() { return is_introductory_price_period; }
///
/// Indicates how much time remains for the introductory price period.
/// Note the store-specific behavior.
///
///
/// Duration remaining in this product's introductory price period.
/// Subscription products with no introductory price period return .
/// Products in the Apple store return if the application does
/// not support iOS version 11.2+, macOS 10.13.2+, or tvOS 11.2+.
/// returned also for products which do not have an introductory period configured.
///
public TimeSpan getIntroductoryPricePeriod() { return introductory_price_period; }
///
/// For subscriptions with an introductory price, get this price.
/// Note the store-specific behavior.
///
///
/// For subscriptions with a introductory price, a localized price string.
/// For Google store the price may not include the currency symbol (e.g. $) and the currency code is available in .
/// For all other product configurations, the string "not available" .
///
///
public string getIntroductoryPrice() { return string.IsNullOrEmpty(introductory_price) ? "not available" : introductory_price; }
///
/// Indicates the number of introductory price billing periods that can be applied to this subscription Product.
/// Note the store-specific behavior.
///
///
/// Products in the Apple store return 0 if the application does not support iOS version 11.2+, macOS 10.13.2+, or tvOS 11.2+.
/// 0 returned also for products which do not have an introductory period configured.
///
///
public long getIntroductoryPricePeriodCycles() { return introductory_price_cycles; }
///
/// When this auto-renewable receipt expires.
///
///
/// An absolute date when this receipt will expire.
///
public DateTime getExpireDate() { return subscriptionExpireDate; }
///
/// When this auto-renewable receipt was canceled.
/// Note the store-specific behavior.
///
///
/// For Apple store, the date when this receipt was canceled.
/// For other stores this will be null .
///
public DateTime getCancelDate() { return subscriptionCancelDate; }
///
/// The period duration of the free trial for this subscription, if enabled.
/// Note the store-specific behavior.
///
///
/// For Google Play store if the product is configured with a free trial, this will be the period duration.
/// For Apple store this will be null .
///
public TimeSpan getFreeTrialPeriod() { return freeTrialPeriod; }
///
/// The duration of this subscription.
/// Note the store-specific behavior.
///
///
/// A duration this subscription is valid for.
/// returned for Apple products.
///
public TimeSpan getSubscriptionPeriod() { return subscriptionPeriod; }
///
/// The string representation of the period in ISO8601 format this subscription is free for.
/// Note the store-specific behavior.
///
///
/// For Google Play store on configured subscription this will be the period which the can own this product for free, unless
/// the user is ineligible for this free trial.
/// For Apple store this will be null .
///
public string getFreeTrialPeriodString() { return free_trial_period_string; }
///
/// The raw JSON SkuDetails from the underlying Google API.
/// Note the store-specific behavior.
/// Note this is not supported.
///
///
/// For Google store the SkuDetails#getOriginalJson results.
/// For Apple this returns null .
///
public string getSkuDetails() { return sku_details; }
///
/// A JSON including a collection of data involving free-trial and introductory prices.
/// Note the store-specific behavior.
/// Used internally for subscription updating on Google store.
///
///
/// A JSON with keys: productId , is_free_trial , is_introductory_price_period , remaining_time_in_seconds .
///
///
public string getSubscriptionInfoJsonString()
{
var dict = new Dictionary
{
{ "productId", productId },
{ "is_free_trial", is_free_trial },
{ "is_introductory_price_period", is_introductory_price_period == Result.True },
{ "remaining_time_in_seconds", remainedTime.TotalSeconds }
};
return MiniJson.JsonEncode(dict);
}
private DateTime nextBillingDate(DateTime billing_begin_date, TimeSpanUnits units)
{
if (units.days == 0.0 && units.months == 0 && units.years == 0)
{
return new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
}
var next_billing_date = billing_begin_date;
// find the next billing date that after the current date
while (DateTime.Compare(next_billing_date, DateTime.UtcNow) <= 0)
{
next_billing_date = next_billing_date.AddDays(units.days).AddMonths(units.months).AddYears(units.years);
}
return next_billing_date;
}
private TimeSpan accumulateIntroductoryDuration(TimeSpanUnits units, long cycles)
{
var result = TimeSpan.Zero;
for (long i = 0; i < cycles; i++)
{
result = result.Add(computePeriodTimeSpan(units));
}
return result;
}
private TimeSpan computePeriodTimeSpan(TimeSpanUnits units)
{
var now = DateTime.Now;
return now.AddDays(units.days).AddMonths(units.months).AddYears(units.years).Subtract(now);
}
private double computeExtraTime(string metadata, double new_sku_period_in_seconds)
{
var wrapper = (Dictionary)MiniJson.JsonDecode(metadata);
var old_sku_remaining_seconds = (long)wrapper["old_sku_remaining_seconds"];
var old_sku_price_in_micros = (long)wrapper["old_sku_price_in_micros"];
var old_sku_period_in_seconds = parseTimeSpan((string)wrapper["old_sku_period_string"]).TotalSeconds;
var new_sku_price_in_micros = (long)wrapper["new_sku_price_in_micros"];
var result = old_sku_remaining_seconds / (double)old_sku_period_in_seconds * old_sku_price_in_micros / new_sku_price_in_micros * new_sku_period_in_seconds;
return result;
}
private TimeSpan parseTimeSpan(string period_string)
{
TimeSpan result;
try
{
result = XmlConvert.ToTimeSpan(period_string);
}
catch (Exception)
{
if (period_string == null || period_string.Length == 0)
{
result = TimeSpan.Zero;
}
else
{
// .Net "P1W" is not supported and throws a FormatException
// not sure if only weekly billing contains "W"
// need more testing
result = new TimeSpan(7, 0, 0, 0);
}
}
return result;
}
private TimeSpanUnits parsePeriodTimeSpanUnits(string time_span)
{
switch (time_span)
{
case "P1W":
// weekly subscription
return new TimeSpanUnits(7.0, 0, 0);
case "P1M":
// monthly subscription
return new TimeSpanUnits(0.0, 1, 0);
case "P3M":
// 3 months subscription
return new TimeSpanUnits(0.0, 3, 0);
case "P6M":
// 6 months subscription
return new TimeSpanUnits(0.0, 6, 0);
case "P1Y":
// yearly subscription
return new TimeSpanUnits(0.0, 0, 1);
default:
// seasonal subscription or duration in days
return new TimeSpanUnits(parseTimeSpan(time_span).Days, 0, 0);
}
}
}
///
/// For representing boolean values which may also be not available.
///
public enum Result
{
///
/// Corresponds to boolean true .
///
True,
///
/// Corresponds to boolean false .
///
False,
///
/// Corresponds to no value, such as for situations where no result is available.
///
Unsupported,
};
///
/// Used internally to parse Apple receipts. Corresponds to Apple SKProductPeriodUnit.
///
///
public enum SubscriptionPeriodUnit
{
///
/// An interval lasting one day.
///
Day = 0,
///
/// An interval lasting one month.
///
Month = 1,
///
/// An interval lasting one week.
///
Week = 2,
///
/// An interval lasting one year.
///
Year = 3,
///
/// Default value when no value is available.
///
NotAvailable = 4,
};
enum AppleStoreProductType
{
NonConsumable = 0,
Consumable = 1,
NonRenewingSubscription = 2,
AutoRenewingSubscription = 3,
};
///
/// Error found during receipt parsing.
///
public class ReceiptParserException : Exception
{
///
/// Construct an error object for receipt parsing.
///
public ReceiptParserException() { }
///
/// Construct an error object for receipt parsing.
///
/// Description of error
public ReceiptParserException(string message) : base(message) { }
}
///
/// An error was found when an invalid is provided.
///
public class InvalidProductTypeException : ReceiptParserException { }
///
/// An error was found when an unexpectedly null is provided.
///
public class NullProductIdException : ReceiptParserException { }
///
/// An error was found when an unexpectedly null is provided.
///
public class NullReceiptException : ReceiptParserException { }
///
/// An error was found when an unsupported app store is provided.
///
public class StoreSubscriptionInfoNotSupportedException : ReceiptParserException
{
///
/// An error was found when an unsupported app store is provided.
///
/// Human readable explanation of this error
public StoreSubscriptionInfoNotSupportedException(string message) : base(message)
{
}
}
}