Search Unity

Unity IAP Subscription info does not include enough details

Discussion in 'Unity IAP' started by clevergamesdev, Dec 4, 2019.

  1. clevergamesdev

    clevergamesdev

    Joined:
    Jun 20, 2019
    Posts:
    17
    Hello,

    I've set up the subscription both in Google & Apple, which includes: 3 days free trial, and then $4 per week.
    I'm trying to display general information about the subscription in my game store, including the free trial days and the price.

    The issue is, with the store extensions, I've managed to get 'freeTrialPeriod' and the 'subscriptionPeriod' only from Android, while in iOS there is no mentioning of the free trial period and the subscription period in their JSON response.

    Here's an example:

    In Android:
    GetProductJSONDictionary() JSON response:
    {"skuDetailsToken":"xxx","productId":"xxx","type":"subs","price":"$4","price_amount_micros":4000000,"price_currency_code":"USD","subscriptionPeriod":"P1W","freeTrialPeriod":"P3D","title":"Premium Membership","description":"Become a premium member!"}

    In iOS:
    GetProductDetails() JSON response:
    {"subscriptionNumberOfUnits":"","subscriptionPeriodUnit":"","localizedPrice":"4","isoCurrencyCode":"USD","localizedPriceString":"$4","localizedTitle":"Premium Membership","localizedDescription":"Become a premium member!","introductoryPrice":"","introductoryPriceLocale":"","introductoryPriceNumberOfPeriods":"","numberOfUnits":"","unit":""}


    If so, how can I sync these details from the store in my game?

    Thanks.
     
  2. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    I will check here, and confirm.
     
  3. clevergamesdev

    clevergamesdev

    Joined:
    Jun 20, 2019
    Posts:
    17
    Hi,
    Any updates regarding this issue?
     
  4. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    Can you show the code you are using? Have you made a purchase, or are you looking for this data, prior to a purchase (most likely).
     
  5. clevergamesdev

    clevergamesdev

    Joined:
    Jun 20, 2019
    Posts:
    17
    I'm looking for this data prior to a purchase, because I need to show general information about the subscription.
    Basic things like how many trial days (3,5,7...) and the period of charging (per week/month/year).

    Android gives me all the information that I'm looking for, but iOS does not.
    I provided exampleS of the responses I get from each platform in my original message.

    Here is the full code for each platform:
    * InAppPurchasing is an helper class.

    Android:
    Code (csharp):
    1.  
    2. var gpExtension = InAppPurchasing.GooglePlayStoreExtensions;
    3. Dictionary<string, string> productJSONDetails = gpExtension.GetProductJSONDictionary();
    4. if (productJSONDetails != null && productJSONDetails.ContainsKey(productId))
    5. {
    6.     var productJSON = productJSONDetails[productId];
    7.     Debug.Log(productJSON);
    8. }
    9.  
    iOS:
    Code (csharp):
    1.  
    2. var asExtension = InAppPurchasing.AppleStoreExtensions;
    3. Dictionary<string, string> productJSONDetails = asExtension.GetProductDetails();
    4. if (productJSONDetails != null && productJSONDetails.ContainsKey(productId))
    5. {
    6.     var productJSON = productJSONDetails[productId];
    7.     Debug.Log(productJSON);
    8. }
    9.  
     
    Last edited: Dec 6, 2019
  6. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    Please try this:

    Dictionary<string, string> introductory_info_dict = m_AppleExtensions.GetIntroductoryPriceDictionary();
    Dictionary<string, string> product_details = m_AppleExtensions.GetProductDetails();
     
  7. clevergamesdev

    clevergamesdev

    Joined:
    Jun 20, 2019
    Posts:
    17
    This is the response I get when calling GetIntroductoryPriceDictionary()

    {
    "introductoryPrice":"",
    "introductoryPriceLocale":"",
    "introductoryPriceNumberOfPeriods":"",
    "numberOfUnits":"",
    "unit":""
    }

    Only to confirm, the subscription is valid and can be purchased without any issue.
    But I wish I had a way to receive these details so my game can always be synced with true data from Apple & Google.
     
    Last edited: Dec 7, 2019
    korimako likes this.
  8. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    We will check here.
     
  9. SamOYUnity3D

    SamOYUnity3D

    Unity Technologies

    Joined:
    May 12, 2019
    Posts:
    626
    Is your subscription an "Auto-Renewable Subscription" or a "Non-Renewing Subscription"?
     
  10. clevergamesdev

    clevergamesdev

    Joined:
    Jun 20, 2019
    Posts:
    17
    Auto-Renewable Subscription
     
  11. korimako

    korimako

    Joined:
    Aug 10, 2013
    Posts:
    40
    I am also trying to retrieve the subscription price and duration from the store before a purchase is made. I can get it post-purchase, but can't find a way to get it pre-purchase - is it possible using Unity iAP?
     
  12. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    Did you try the code mentioned previously? I haven't tested it in awhile however.
     
  13. korimako

    korimako

    Joined:
    Aug 10, 2013
    Posts:
    40
    Thanks, I got it working. I am going to post the code I used here in case it helps someone because there were a number of things to work out beyond those few lines.

    Firstly, I made a ProductInfo class to store the JSON info in. I included a SubscriptionPeriods dictionary to translate the subscriptionPeriodUnit to a readable string. I tried to use SubscriptionPeriodUnit but it didn't correspond to the values I was getting (2 = month), is that a bug? It seems like month and week are swapped.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [System.Serializable]
    6. public class ProductInfo
    7. {
    8.     public string subscriptionNumberOfUnits;
    9.     public string subscriptionPeriodUnit = "";
    10.     public string localizedPrice;
    11.     public string isoCurrencyCode;
    12.     public string localizedPriceString;
    13.     public string localizedTitle;
    14.     public string localizedDescription;
    15.     public string introductoryPriceLocale;
    16.     public string introductoryPriceNumberOfPeriods;
    17.     public string numberOfUnits;
    18.     public string unit;
    19.  
    20.     // I have only tested 2 = month, I have not tested other values in this list
    21.     public Dictionary<string, string> SubscriptionPeriods = new Dictionary<string, string>()
    22.     {
    23.         {"0", "day" },
    24.         {"1", "week" },
    25.         {"2", "month" },
    26.         {"3", "year" },
    27.         {"4", "not available" }
    28.     };
    29.  
    30.     public static ProductInfo CreateFromJSON(string jsonString)
    31.     {
    32.         return JsonUtility.FromJson<ProductInfo>(jsonString);
    33.     }
    34.  
    35.     public string GetSubscriptionPeriod()
    36.     {
    37.         if(this.subscriptionPeriodUnit == "") { return ""; };
    38.         return SubscriptionPeriods[this.subscriptionPeriodUnit];
    39.  
    40.     }
    41.  
    42.     //IAP Product details productJSON {"subscriptionNumberOfUnits":"2","subscriptionPeriodUnit":"2","localizedPrice":"0.99","isoCurrencyCode":"USD","localizedPriceString":"$0.99","localizedTitle":"IAP Title","localizedDescription":"Enjoy the subscription!","introductoryPrice":"","introductoryPriceLocale":"","introductoryPriceNumberOfPeriods":"","numberOfUnits":"","unit":""}
    43.  
    44. }
    Secondly, using the Version 2 example in this thread, I used this code, see OnInitialized for the most relevant bit, this is still a work in progress, but I am 99% sure I am successfully retrieving product info before purchasing:

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Purchasing;
    5. using UnityEngine.Purchasing.Security;
    6.  
    7. // Deriving the Purchaser class from IStoreListener enables it to receive messages from Unity Purchasing.
    8. public class IAPManager : Singleton<IAPManager>, IStoreListener
    9. {
    10.     private static IStoreController m_StoreController;          // The Unity Purchasing system.
    11.     private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
    12.     private IAppleExtensions m_AppleExtensions;
    13.     private IGooglePlayStoreExtensions m_GoogleExtensions;
    14.  
    15.     // PRODUCT IDs
    16.     public string OWN_FOREVER = "NON_CONSUMABLE_ID";
    17.     public string SUBSCRIPTION = "SUBSCRIPTION_ID";
    18.     private WithTrialEdition DataManager;
    19.  
    20.     public Dictionary<string, ProductInfo> ProductsInStore = new Dictionary<string, ProductInfo>();
    21.  
    22.     void Start()
    23.     {
    24.         // Set our data manager
    25.         DataManager = GameObject.Find("WithTrialEdition").GetComponent<WithTrialEdition>();
    26.  
    27.         if (DataManager.HasPurchasedApp())
    28.         {
    29.             // if before we even start the data manager sees that planetscene.OwnsApp is true then this may not be the Trial version.
    30.             return;
    31.         }
    32.  
    33.         if (m_StoreController == null)
    34.         {
    35.             InitializePurchasing();
    36.         }
    37.     }
    38.  
    39.     public void InitializePurchasing()
    40.     {
    41.         if (IsInitialized())
    42.         {
    43.             return;
    44.         }
    45.         var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    46.         builder.AddProduct(OWN_FOREVER, ProductType.NonConsumable);
    47.         builder.AddProduct(SUBSCRIPTION, ProductType.Subscription);
    48.         UnityPurchasing.Initialize(this, builder);
    49.     }
    50.  
    51.  
    52.     public bool IsInitialized()
    53.     {
    54.         return m_StoreController != null && m_StoreExtensionProvider != null;
    55.     }
    56.  
    57.  
    58.     public void BuyForever()
    59.     {
    60.         Debug.Log("IAP Buy Forever");
    61.         BuyProductID(OWN_FOREVER);
    62.     }
    63.  
    64.     public void BuySubscription()
    65.     {
    66.         Debug.Log("IAP Buy Subscription");
    67.         BuyProductID(SUBSCRIPTION);
    68.     }
    69.  
    70.     public string GetProductPriceFromStore(string id)
    71.     {
    72.         if (m_StoreController != null && m_StoreController.products != null)
    73.             return m_StoreController.products.WithID(id).metadata.localizedPriceString;
    74.         else
    75.             return "";
    76.     }
    77.  
    78.     public string GetProductSubscriptionDurationUnit(string id)
    79.     {
    80.         if (ProductsInStore.ContainsKey(id))
    81.         {
    82.             return ProductsInStore[id].GetSubscriptionPeriod();
    83.         }
    84.         return "";
    85.     }
    86.  
    87.     public int GetProductSubscriptionDurationAmt(string id)
    88.     {
    89.         if (ProductsInStore.ContainsKey(id))
    90.         {
    91.             int result;
    92.             if (int.TryParse(ProductsInStore[id].subscriptionNumberOfUnits, out result))
    93.             {
    94.                 return result;
    95.             }
    96.             else
    97.             {
    98.                 return -1;
    99.             }
    100.         }
    101.         return -1;
    102.     }
    103.  
    104.     void BuyProductID(string productId)
    105.     {
    106.         if (IsInitialized())
    107.         {
    108.             Product product = m_StoreController.products.WithID(productId);
    109.             if (product != null && product.availableToPurchase)
    110.             {
    111.                 Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));
    112.                 m_StoreController.InitiatePurchase(product);
    113.             }
    114.             else
    115.             {
    116.                 Debug.Log("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
    117.             }
    118.         }
    119.         else
    120.         {
    121.             Debug.Log("BuyProductID FAIL. Not initialized.");
    122.         }
    123.     }
    124.  
    125.  
    126.     public void RestorePurchases()
    127.     {
    128.         m_StoreExtensionProvider.GetExtension<IAppleExtensions>().RestoreTransactions(result => {
    129.             if (result)
    130.             {
    131.                 Debug.Log("Restore purchases succeeded.");
    132.             }
    133.             else
    134.             {
    135.                 Debug.Log("Restore purchases failed.");
    136.             }
    137.         });
    138.     }
    139.  
    140.     //
    141.     // --- IStoreListener
    142.     //
    143.  
    144.     public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    145.     {
    146.         // Purchasing has succeeded initializing. Collect our Purchasing references.
    147.         Debug.Log("IAP OnInitialized: PASS");
    148.  
    149.         // Overall Purchasing system, configured with products for this application.
    150.         m_StoreController = controller;
    151.         // Store specific subsystem, for accessing device-specific store features.
    152.         m_StoreExtensionProvider = extensions;
    153.  
    154.         m_AppleExtensions = extensions.GetExtension<IAppleExtensions>();
    155.         m_GoogleExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();
    156.  
    157.         m_GoogleExtensions?.SetDeferredPurchaseListener(OnPurchaseDeferred);
    158.  
    159.         Dictionary<string, string> dict = m_AppleExtensions.GetIntroductoryPriceDictionary();
    160.  
    161.         Dictionary<string, string> product_details = m_AppleExtensions.GetProductDetails();
    162.  
    163.         bool isSubscribed = false;
    164.         bool hasPurchasedForever = false;
    165.  
    166.         foreach (UnityEngine.Purchasing.Product item in controller.products.all)
    167.         {
    168.  
    169.             if (product_details != null && product_details.ContainsKey(item.definition.storeSpecificId))
    170.             {
    171.                 var productJSON = product_details[item.definition.storeSpecificId];
    172.                 ProductInfo productInfo = ProductInfo.CreateFromJSON(productJSON);
    173.                 Debug.Log("IAP Product details productJSON" + productJSON);
    174.                 ProductsInStore.Add(item.definition.storeSpecificId, productInfo);
    175.                 Debug.Log("IAP Product details productInfo.subscriptionPeriodUnit" + productInfo.subscriptionPeriodUnit);
    176.                 Debug.Log("IAP Product details productInfo.subscriptionNumberOfUnits" + productInfo.subscriptionNumberOfUnits);
    177.                 Debug.Log("IAP Product details productInfo.GetSubscriptionPeriod()" + productInfo.GetSubscriptionPeriod());
    178.             }
    179.  
    180.  
    181.             if (item.receipt != null)
    182.             {
    183.                 // ITEMS WE HAVE A RECEIPT FOR
    184.  
    185.                 string intro_json = (dict == null || !dict.ContainsKey(item.definition.storeSpecificId)) ? null : dict[item.definition.storeSpecificId];
    186.  
    187.                 if (item.definition.type == ProductType.Subscription)
    188.                 {
    189.                     SubscriptionManager p = new SubscriptionManager(item, intro_json);
    190.                     SubscriptionInfo info = p.getSubscriptionInfo();
    191.                     Debug.Log("IAP SubInfo: " + info.getProductId().ToString());
    192.                     Debug.Log("IAP isSubscribed: " + info.isSubscribed().ToString());
    193.                     Debug.Log("IAP isFreeTrial: " + info.isFreeTrial().ToString());
    194.                     Debug.Log("IAP info.getSubscriptionPeriod" + info.getSubscriptionPeriod());
    195.  
    196.                     if(info.isSubscribed() == Result.True)
    197.                     {
    198.                         isSubscribed = true;
    199.                     }
    200.                 } else if (item.definition.type == ProductType.NonConsumable)
    201.                 {
    202.                     if(String.Equals(item.definition.id, OWN_FOREVER, StringComparison.Ordinal))
    203.                     {
    204.                         hasPurchasedForever = true;
    205.                     }
    206.                 }
    207.             }
    208.  
    209.         }
    210.  
    211.         Debug.Log("IAP isSubscribed: " + isSubscribed);
    212.         Debug.Log("IAP hasPurchasedForever: " + hasPurchasedForever);
    213.  
    214.     }
    215.  
    216.     public void OnPurchaseDeferred(Product product)
    217.     {
    218.  
    219.         Debug.Log("Deferred product " + product.definition.id.ToString());
    220.     }
    221.  
    222.  
    223.     public void OnInitializeFailed(InitializationFailureReason error)
    224.     {
    225.         // Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
    226.         Debug.Log("IAP OnInitializeFailed InitializationFailureReason:" + error);
    227.     }
    228.  
    229.  
    230.  
    231.     public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
    232.     {
    233.  
    234.         Debug.Log(string.Format("IAP ProcessPurchase"));
    235.  
    236.         try
    237.         {
    238.             var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
    239.             var result = validator.Validate(args.purchasedProduct.receipt);
    240.             Debug.Log("IAP Validate = " + result.ToString());
    241.  
    242.             foreach (IPurchaseReceipt productReceipt in result)
    243.             {
    244.                 Debug.Log("IAP Valid receipt for " + productReceipt.productID.ToString());
    245.  
    246.  
    247.                 if (String.Equals(productReceipt.productID.ToString(), OWN_FOREVER, StringComparison.Ordinal))
    248.                 {
    249.                     Debug.Log(string.Format("IAP ProcessPurchase: PASS. Product: '{0}'", productReceipt.productID.ToString()));
    250.                     DataManager.OnPurchasedForever();
    251.                 }
    252.                 else if (String.Equals(productReceipt.productID.ToString(), SUBSCRIPTION, StringComparison.Ordinal))
    253.                 {
    254.                     Debug.Log(string.Format("IAP ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
    255.                     DataManager.OnPurchasedSubscription();
    256.                 }
    257.  
    258.  
    259.  
    260.             }
    261.         }
    262.         catch (Exception e)
    263.         {
    264.             Debug.Log("IAP Error is " + e.Message.ToString());
    265.         }
    266.  
    267.         Debug.Log(string.Format("IAP ProcessPurchase: " + args.purchasedProduct.definition.id));
    268.  
    269.         return PurchaseProcessingResult.Complete;
    270.  
    271.     }
    272.  
    273.     public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    274.     {
    275.         // A product purchase attempt did not succeed. Check failureReason for more detail. Consider sharing
    276.         // this reason with the user to guide their troubleshooting actions.
    277.         Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
    278.     }
    279. }
    280.  
    I hope this helps someone, Unity IAP is not simple to navigate!