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) { } } }