1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090 |
- 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
- {
-
- /// <summary>
- /// 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.
- /// </summary>
- public class TimeSpanUnits
- {
- /// <summary>
- /// Discrete duration in days, if less than a month, otherwise zero.
- /// </summary>
- public double days;
- /// <summary>
- /// Discrete duration in months, if less than a year, otherwise zero.
- /// </summary>
- public int months;
- /// <summary>
- /// Discrete duration in years, otherwise zero.
- /// </summary>
- public int years;
-
- /// <summary>
- /// Construct a subscription duration.
- /// </summary>
- /// <param name="d">Discrete duration in days, if less than a month, otherwise zero.</param>
- /// <param name="m">Discrete duration in months, if less than a year, otherwise zero.</param>
- /// <param name="y">Discrete duration in years, otherwise zero.</param>
- public TimeSpanUnits(double d, int m, int y)
- {
- days = d;
- months = m;
- years = y;
- }
- }
-
- /// <summary>
- /// 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.
- /// </summary>
- /// <seealso cref="IAppleExtensions.GetIntroductoryPriceDictionary"/>
- /// <seealso cref="UpdateSubscription"/>
- public class SubscriptionManager
- {
-
- private readonly string receipt;
- private readonly string productId;
- private readonly string intro_json;
-
- /// <summary>
- /// Performs subscription updating, migrating a subscription into another as long as they are both members
- /// of the same subscription group on the App Store.
- /// </summary>
- /// <param name="newProduct">Destination subscription product, belonging to the same subscription group as <paramref name="oldProduct"/></param>
- /// <param name="oldProduct">Source subscription product, belonging to the same subscription group as <paramref name="newProduct"/></param>
- /// <param name="developerPayload">Carried-over metadata from prior call to <typeparamref name="SubscriptionManager.UpdateSubscription"/> </param>
- /// <param name="appleStore">Triggered upon completion of the subscription update.</param>
- /// <param name="googleStore">Triggered upon completion of the subscription update.</param>
- public static void UpdateSubscription(Product newProduct, Product oldProduct, string developerPayload, Action<Product, string> appleStore, Action<string, string> 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<string, object>)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;
- }
- }
- }
- }
-
- /// <summary>
- /// Performs subscription updating, migrating a subscription into another as long as they are both members
- /// of the same subscription group on the App Store.
- /// </summary>
- /// <param name="oldProduct">Source subscription product, belonging to the same subscription group as <paramref name="newProduct"/></param>
- /// <param name="newProduct">Destination subscription product, belonging to the same subscription group as <paramref name="oldProduct"/></param>
- /// <param name="googlePlayUpdateCallback">Triggered upon completion of the subscription update.</param>
- public static void UpdateSubscriptionInGooglePlayStore(Product oldProduct, Product newProduct, Action<string, string> 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);
- }
-
- /// <summary>
- /// Performs subscription updating, migrating a subscription into another as long as they are both members
- /// of the same subscription group on the App Store.
- /// </summary>
- /// <param name="newProduct">Destination subscription product, belonging to the same subscription group as <paramref name="oldProduct"/></param>
- /// <param name="developerPayload">Carried-over metadata from prior call to <typeparamref name="SubscriptionManager.UpdateSubscription"/> </param>
- /// <param name="appleStoreUpdateCallback">Triggered upon completion of the subscription update.</param>
- public static void UpdateSubscriptionInAppleStore(Product newProduct, string developerPayload, Action<Product, string> appleStoreUpdateCallback)
- {
- appleStoreUpdateCallback(newProduct, developerPayload);
- }
-
- /// <summary>
- /// Construct an object that allows inspection of a subscription product.
- /// </summary>
- /// <param name="product">Subscription to be inspected</param>
- /// <param name="intro_json">From <typeparamref name="IAppleExtensions.GetIntroductoryPriceDictionary"/></param>
- public SubscriptionManager(Product product, string intro_json)
- {
- receipt = product.receipt;
- productId = product.definition.storeSpecificId;
- this.intro_json = intro_json;
- }
-
- /// <summary>
- /// Construct an object that allows inspection of a subscription product.
- /// </summary>
- /// <param name="receipt">A Unity IAP unified receipt from <typeparamref name="Product.receipt"/></param>
- /// <param name="id">A product identifier.</param>
- /// <param name="intro_json">From <typeparamref name="IAppleExtensions.GetIntroductoryPriceDictionary"/></param>
- public SubscriptionManager(string receipt, string id, string intro_json)
- {
- this.receipt = receipt;
- productId = id;
- this.intro_json = intro_json;
- }
-
- /// <summary>
- /// Convert my Product into a <typeparamref name="SubscriptionInfo"/>.
- /// My Product.receipt must have a "Payload" JSON key containing supported native app store
- /// information, which will be converted here.
- /// </summary>
- /// <returns></returns>
- /// <exception cref="NullProductIdException">My Product must have a non-null product identifier</exception>
- /// <exception cref="StoreSubscriptionInfoNotSupportedException">A supported app store must be used as my product</exception>
- /// <exception cref="NullReceiptException">My product must have</exception>
- public SubscriptionInfo getSubscriptionInfo()
- {
-
- if (receipt != null)
- {
- var receipt_wrapper = (Dictionary<string, object>)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<AppleInAppPurchaseReceipt>();
-
- 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<AppleInAppPurchaseReceipt> receipts)
- {
- receipts.Sort((b, a) => a.purchaseDate.CompareTo(b.purchaseDate));
- return receipts[0];
- }
-
- private SubscriptionInfo getGooglePlayStoreSubInfo(string payload)
- {
- var payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(payload);
- payload_wrapper.TryGetValue("skuDetails", out var skuDetailsObject);
-
- var skuDetails = (skuDetailsObject as List<object>)?.Select(obj => obj as string);
-
- var purchaseHistorySupported = false;
-
- var original_json_payload_wrapper =
- (Dictionary<string, object>)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<string, object>)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);
- }
- }
-
- /// <summary>
- /// A container for a Product’s subscription-related information.
- /// </summary>
- /// <seealso cref="SubscriptionManager.getSubscriptionInfo"/>
- 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;
-
- /// <summary>
- /// Unpack Apple receipt subscription data.
- /// </summary>
- /// <param name="r">The Apple receipt from <typeparamref name="CrossPlatformValidator"/></param>
- /// <param name="intro_json">From <typeparamref name="IAppleExtensions.GetIntroductoryPriceDictionary"/>. Keys:
- /// <c>introductoryPriceLocale</c>, <c>introductoryPrice</c>, <c>introductoryPriceNumberOfPeriods</c>, <c>numberOfUnits</c>,
- /// <c>unit</c>, which can be fetched from Apple's remote service.</param>
- /// <exception cref="InvalidProductTypeException">Error found involving an invalid product type.</exception>
- /// <see cref="CrossPlatformValidator"/>
- 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<string, object>)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;
-
-
- }
-
- /// <summary>
- /// Especially crucial values relating to Google subscription products.
- /// Note this is intended to be called internally.
- /// </summary>
- /// <param name="skuDetails">The raw JSON from <c>SkuDetail.getOriginalJson</c></param>
- /// <param name="isAutoRenewing">Whether this subscription is expected to auto-renew</param>
- /// <param name="purchaseDate">A date this subscription was billed</param>
- /// <param name="isFreeTrial">Indicates whether this Product is a free trial</param>
- /// <param name="hasIntroductoryPriceTrial">Indicates whether this Product may be owned with an introductory price period.</param>
- /// <param name="purchaseHistorySupported">Unsupported</param>
- /// <param name="updateMetadata">Unsupported. Mechanism previously propagated subscription upgrade information to new subscription. </param>
- /// <exception cref="InvalidProductTypeException">For non-subscription product types. </exception>
- public SubscriptionInfo(string skuDetails, bool isAutoRenewing, DateTime purchaseDate, bool isFreeTrial,
- bool hasIntroductoryPriceTrial, bool purchaseHistorySupported, string updateMetadata)
- {
-
- var skuDetails_wrapper = (Dictionary<string, object>)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;
- }
-
- /// <summary>
- /// Especially crucial values relating to subscription products.
- /// Note this is intended to be called internally.
- /// </summary>
- /// <param name="productId">This subscription's product identifier</param>
- 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;
- }
-
- /// <summary>
- /// Store specific product identifier.
- /// </summary>
- /// <returns>The product identifier from the store receipt.</returns>
- public string getProductId() { return productId; }
-
- /// <summary>
- /// A date this subscription was billed.
- /// Note the store-specific behavior.
- /// </summary>
- /// <returns>
- /// 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.
- /// </returns>
- public DateTime getPurchaseDate() { return purchaseDate; }
-
- /// <summary>
- /// 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.
- /// </summary>
- /// <returns>
- /// <typeparamref name="Result.True"/> Subscription status if the store receipt's expiration date is
- /// after the device's current time.
- /// <typeparamref name="Result.False"/> otherwise.
- /// Non-renewable subscriptions in the Apple store return a <typeparamref name="Result.Unsupported"/> value.
- /// </returns>
- /// <seealso cref="isExpired"/>
- /// <seealso cref="DateTime.UtcNow"/>
- public Result isSubscribed() { return is_subscribed; }
-
- /// <summary>
- /// 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.
- /// </summary>
- /// <returns>
- /// <typeparamref name="Result.True"/> Subscription status if the store receipt's expiration date is
- /// before the device's current time.
- /// <typeparamref name="Result.False"/> otherwise.
- /// Non-renewable subscriptions in the Apple store return a <typeparamref name="Result.Unsupported"/> value.
- /// </returns>
- /// <seealso cref="isSubscribed"/>
- /// <seealso cref="DateTime.UtcNow"/>
- public Result isExpired() { return is_expired; }
-
- /// <summary>
- /// 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.
- /// </summary>
- /// <returns>
- /// <typeparamref name="Result.True"/> Cancellation status if the store receipt's indicates this subscription is cancelled.
- /// <typeparamref name="Result.False"/> otherwise.
- /// Non-renewable subscriptions in the Apple store return a <typeparamref name="Result.Unsupported"/> value.
- /// </returns>
- public Result isCancelled() { return is_cancelled; }
-
- /// <summary>
- /// Indicates whether this Product is a free trial.
- /// Note the store-specific behavior.
- /// </summary>
- /// <returns>
- /// <typeparamref name="Result.True"/> This subscription is a free trial according to the store receipt.
- /// <typeparamref name="Result.False"/> 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 <typeparamref name="Result.Unsupported"/> value.
- /// </returns>
- public Result isFreeTrial() { return is_free_trial; }
-
- /// <summary>
- /// Indicates whether this Product is expected to auto-renew. The product must be auto-renewable, not canceled, and not expired.
- /// </summary>
- /// <returns>
- /// <typeparamref name="Result.True"/> The store receipt's indicates this subscription is auto-renewing.
- /// <typeparamref name="Result.False"/> The store receipt's indicates this subscription is not auto-renewing.
- /// Non-renewable subscriptions in the Apple store return a <typeparamref name="Result.Unsupported"/> value.
- /// </returns>
- public Result isAutoRenewing() { return is_auto_renewing; }
-
- /// <summary>
- /// 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.
- /// </summary>
- /// <returns>
- /// 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 <typeparamref name="TimeSpan.MaxValue"/>.
- /// </returns>
- /// <seealso cref="DateTime.UtcNow"/>
- public TimeSpan getRemainingTime() { return remainedTime; }
-
- /// <summary>
- /// Indicates whether this Product is currently owned within an introductory price period.
- /// Note the store-specific behavior.
- /// </summary>
- /// <returns>
- /// <typeparamref name="Result.True"/> The store receipt's indicates this subscription is within its introductory price period.
- /// <typeparamref name="Result.False"/> The store receipt's indicates this subscription is not within its introductory price period.
- /// <typeparamref name="Result.False"/> If the product is not configured to have an introductory period.
- /// Non-renewable subscriptions in the Apple store return a <typeparamref name="Result.Unsupported"/> value.
- /// Google subscriptions queried on devices with version lower than 6 of the Android in-app billing API return a <typeparamref name="Result.Unsupported"/> value.
- /// </returns>
- public Result isIntroductoryPricePeriod() { return is_introductory_price_period; }
-
- /// <summary>
- /// Indicates how much time remains for the introductory price period.
- /// Note the store-specific behavior.
- /// </summary>
- /// <returns>
- /// Duration remaining in this product's introductory price period.
- /// Subscription products with no introductory price period return <typeparamref name="TimeSpan.Zero"/>.
- /// Products in the Apple store return <typeparamref name="TimeSpan.Zero"/> if the application does
- /// not support iOS version 11.2+, macOS 10.13.2+, or tvOS 11.2+.
- /// <typeparamref name="TimeSpan.Zero"/> returned also for products which do not have an introductory period configured.
- /// </returns>
- public TimeSpan getIntroductoryPricePeriod() { return introductory_price_period; }
-
- /// <summary>
- /// For subscriptions with an introductory price, get this price.
- /// Note the store-specific behavior.
- /// </summary>
- /// <returns>
- /// 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 <typeparamref name="ProductMetadata.isoCurrencyCode"/>.
- /// For all other product configurations, the string <c>"not available"</c>.
- /// </returns>
- /// <seealso cref="ProductMetadata.isoCurrencyCode"/>
- public string getIntroductoryPrice() { return string.IsNullOrEmpty(introductory_price) ? "not available" : introductory_price; }
-
- /// <summary>
- /// Indicates the number of introductory price billing periods that can be applied to this subscription Product.
- /// Note the store-specific behavior.
- /// </summary>
- /// <returns>
- /// Products in the Apple store return <c>0</c> if the application does not support iOS version 11.2+, macOS 10.13.2+, or tvOS 11.2+.
- /// <c>0</c> returned also for products which do not have an introductory period configured.
- /// </returns>
- /// <seealso cref="intro"/>
- public long getIntroductoryPricePeriodCycles() { return introductory_price_cycles; }
-
- /// <summary>
- /// When this auto-renewable receipt expires.
- /// </summary>
- /// <returns>
- /// An absolute date when this receipt will expire.
- /// </returns>
- public DateTime getExpireDate() { return subscriptionExpireDate; }
-
- /// <summary>
- /// When this auto-renewable receipt was canceled.
- /// Note the store-specific behavior.
- /// </summary>
- /// <returns>
- /// For Apple store, the date when this receipt was canceled.
- /// For other stores this will be <c>null</c>.
- /// </returns>
- public DateTime getCancelDate() { return subscriptionCancelDate; }
-
- /// <summary>
- /// The period duration of the free trial for this subscription, if enabled.
- /// Note the store-specific behavior.
- /// </summary>
- /// <returns>
- /// 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 <c> null </c>.
- /// </returns>
- public TimeSpan getFreeTrialPeriod() { return freeTrialPeriod; }
-
- /// <summary>
- /// The duration of this subscription.
- /// Note the store-specific behavior.
- /// </summary>
- /// <returns>
- /// A duration this subscription is valid for.
- /// <typeparamref name="TimeSpan.Zero"/> returned for Apple products.
- /// </returns>
- public TimeSpan getSubscriptionPeriod() { return subscriptionPeriod; }
-
- /// <summary>
- /// The string representation of the period in ISO8601 format this subscription is free for.
- /// Note the store-specific behavior.
- /// </summary>
- /// <returns>
- /// 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 <c> null </c>.
- /// </returns>
- public string getFreeTrialPeriodString() { return free_trial_period_string; }
-
- /// <summary>
- /// The raw JSON SkuDetails from the underlying Google API.
- /// Note the store-specific behavior.
- /// Note this is not supported.
- /// </summary>
- /// <returns>
- /// For Google store the <c> SkuDetails#getOriginalJson </c> results.
- /// For Apple this returns <c>null</c>.
- /// </returns>
- public string getSkuDetails() { return sku_details; }
-
- /// <summary>
- /// 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.
- /// </summary>
- /// <returns>
- /// A JSON with keys: <c>productId</c>, <c>is_free_trial</c>, <c>is_introductory_price_period</c>, <c>remaining_time_in_seconds</c>.
- /// </returns>
- /// <seealso cref="SubscriptionManager.UpdateSubscription"/>
- public string getSubscriptionInfoJsonString()
- {
- var dict = new Dictionary<string, object>
- {
- { "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<string, object>)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);
- }
- }
-
-
- }
-
-
- /// <summary>
- /// For representing boolean values which may also be not available.
- /// </summary>
- public enum Result
- {
- /// <summary>
- /// Corresponds to boolean <c> true </c>.
- /// </summary>
- True,
- /// <summary>
- /// Corresponds to boolean <c> false </c>.
- /// </summary>
- False,
- /// <summary>
- /// Corresponds to no value, such as for situations where no result is available.
- /// </summary>
- Unsupported,
- };
-
- /// <summary>
- /// Used internally to parse Apple receipts. Corresponds to Apple SKProductPeriodUnit.
- /// </summary>
- /// <see cref="https://developer.apple.com/documentation/storekit/skproductperiodunit?language=objc"/>
- public enum SubscriptionPeriodUnit
- {
- /// <summary>
- /// An interval lasting one day.
- /// </summary>
- Day = 0,
- /// <summary>
- /// An interval lasting one month.
- /// </summary>
- Month = 1,
- /// <summary>
- /// An interval lasting one week.
- /// </summary>
- Week = 2,
- /// <summary>
- /// An interval lasting one year.
- /// </summary>
- Year = 3,
- /// <summary>
- /// Default value when no value is available.
- /// </summary>
- NotAvailable = 4,
- };
-
- enum AppleStoreProductType
- {
- NonConsumable = 0,
- Consumable = 1,
- NonRenewingSubscription = 2,
- AutoRenewingSubscription = 3,
- };
-
- /// <summary>
- /// Error found during receipt parsing.
- /// </summary>
- public class ReceiptParserException : Exception
- {
- /// <summary>
- /// Construct an error object for receipt parsing.
- /// </summary>
- public ReceiptParserException() { }
-
- /// <summary>
- /// Construct an error object for receipt parsing.
- /// </summary>
- /// <param name="message">Description of error</param>
- public ReceiptParserException(string message) : base(message) { }
- }
-
- /// <summary>
- /// An error was found when an invalid <typeparamref name="Product.definition.type"/> is provided.
- /// </summary>
- public class InvalidProductTypeException : ReceiptParserException { }
-
- /// <summary>
- /// An error was found when an unexpectedly null <typeparamref name="Product.definition.id"/> is provided.
- /// </summary>
- public class NullProductIdException : ReceiptParserException { }
-
- /// <summary>
- /// An error was found when an unexpectedly null <typeparamref name="Product.receipt"/> is provided.
- /// </summary>
- public class NullReceiptException : ReceiptParserException { }
-
- /// <summary>
- /// An error was found when an unsupported app store <typeparamref name="Product.receipt"/> is provided.
- /// </summary>
- public class StoreSubscriptionInfoNotSupportedException : ReceiptParserException
- {
- /// <summary>
- /// An error was found when an unsupported app store <typeparamref name="Product.receipt"/> is provided.
- /// </summary>
- /// <param name="message">Human readable explanation of this error</param>
- public StoreSubscriptionInfoNotSupportedException(string message) : base(message)
- {
- }
- }
- }
|