Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

How to verify that a user has a valid subscription.

Discussion in 'Unity IAP' started by Calamity_Jones, Feb 1, 2021.

Thread Status:
Not open for further replies.
  1. Calamity_Jones

    Calamity_Jones

    Joined:
    Jul 31, 2019
    Posts:
    25
    So, let me begin with a few complaints, as figuring this out has been by far the most confusing, frustrating and difficult aspect of the app I'm working on...

    I find it very disappointing that codeless IAP does not support verifying active subscriptions (unless I'm mistaken) - I was able to easily build a fully-functional IAP system where the user can buy a subscription, but I couldn't figure out how to verify that their subscription is still active after the subscription duration expires, and the user pays for the next month.

    I've since then switched to a code implementation of IAP, using the IAP demo script (which is very challenging to read and understand - though I guess it was written to cover every usage case). Again, I have built a system that allows me to buy a subscription, but I'm still unsure of how to verify that the user has an active subscription.

    My understanding of the process is as follows:
    1. We define subscriptions for the Play/App stores. For this example, we’ll have a 1-month subscription priced at 4.99.

    2. User purchases a subscription product in the app, pays the 4.99 fee.

    3. I get the confirmation through PurchaseProcessingResult, and I send a message to our server, setting Subscribed to true. I then unlock all premium features for the user.

    4. The next time the user logs into the app, I get the Subscribed bool from the server, it returns true, and I unlock all premium features for them. All is functioning as expected.

    5. After a month has passed, the user is billed 4.99 again, or if they cancel their subscription before this, they can continue to use the premium features until the end of the month they have paid for.

    6. At this point, I’m running into a wall…
    I understand that purchases are restored automatically on Android apps, and on a button press for iOS apps – though I believe that this only happens the first time you run the app following a fresh install. Is this correct? In this case, I presume that this is not very relevant to subscriptions, since they are a rolling contract. This would be more useful if you had (for example) a one-time “remove ads” purchase.


    I’ve enabled the subscription manager code, but I don’t understand what a subscription manager is. Where does it exist? Is it just a way of formatting some data? It’s inside OnInitialized, and reads the receipt of an item… So does that mean that when the user buys the subscription, their receipt is stored on their account in the App/Play store, and I’m retrieving this in the app. This receipt then contains a bunch of data such as isSubscribed, isCancelled, etc. I simply must read that data from the receipt? Is the receipt renewed every time the user is billed? Is there a way of testing this in TestFlight? My understanding is that purchases completed in TestFlight “vanish” as soon as you close the app.


    Finally, what is a payload? I found a stackoverflow post that mentions that subscription managers no longer work with the play store, and mentions developer payloads. If this is the case, how do I verify subscriptions on Android?

    https://stackoverflow.com/questions...date-subscriptions-using-unity-iap-with-the-n


    Thanks in advance for any help!
     
    joseGuate97 likes this.
  2. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
  3. Calamity_Jones

    Calamity_Jones

    Joined:
    Jul 31, 2019
    Posts:
    25
    Is it possible to test this in TestFlight? As I mentioned, I think that the purchases "vanish" when the app closes.
     
  4. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    Absolutely! I test in TestFlight every day using it. Make sure to have a Restore button on iOS https://docs.unity3d.com/Manual/UnityIAPRestoringTransactions.html The code shows it happening automatically, but it needs its own button, an Apple requirement.
     
    Last edited: Feb 1, 2021
  5. Calamity_Jones

    Calamity_Jones

    Joined:
    Jul 31, 2019
    Posts:
    25
    Okay, so regarding the restore transaction button - I believe it calls ProcessPurchase as though it were a new transaction. I'm not seeing anything happen here, because (unless I'm mistaken) every time you run the app on testflight, it's as though you're using a completely fresh apple account? This is what I meant about subscriptions vanishing. I'm able to test the IAP features on Testflight APART from verifying existing subscriptions - I can BUY a subscription, but I'm not seeing anything come up when I check for an existing receipt - i.e. for an active subscription.

    This is the code I'm using, which is just a slight modification of the IAPDemo script included in Unity IAP.

    N.B. PrintDebugString is just Debug.Log, but I'm checking against a "DebugMode" bool. Other than that, I haven't really changed anything in the IAPDemo script.

    Code (CSharp):
    1. #if SUBSCRIPTION_MANAGER
    2.                 // this is the usage of SubscriptionManager class
    3.                 if (item.receipt != null)
    4.                 {
    5.                     if (item.definition.type == ProductType.Subscription)
    6.                     {
    7.                         if (checkIfProductIsAvailableForSubscriptionManager(item.receipt))
    8.                         {
    9.                             string intro_json = (introductory_info_dict == null || !introductory_info_dict.ContainsKey(item.definition.storeSpecificId)) ? null : introductory_info_dict[item.definition.storeSpecificId];
    10.                             SubscriptionManager p = new SubscriptionManager(item, intro_json);
    11.                             SubscriptionInfo info = p.getSubscriptionInfo();
    12.  
    13.                             //--------------------------------------TESTING-------------------------------------
    14.                             if (info.isSubscribed() == Result.True)
    15.                             {
    16.                                 // grant premium
    17.                                 PrintDebugString("user is subscribed.");
    18.                             }
    19.                             else
    20.                             {
    21.                                 // revoke premium
    22.                                 PrintDebugString("user is not subscribed.");
    23.                             }
    24.  
    25.                             PrintDebugString("product id is: " + info.getProductId());
    26.                             PrintDebugString("purchase date is: " + info.getPurchaseDate());
    27.                             PrintDebugString("subscription next billing date is: " + info.getExpireDate());
    28.                             PrintDebugString("is subscribed? " + info.isSubscribed().ToString());
    29.                             PrintDebugString("is expired? " + info.isExpired().ToString());
    30.                             PrintDebugString("is cancelled? " + info.isCancelled());
    31.                             PrintDebugString("product is in free trial peroid? " + info.isFreeTrial());
    32.                             PrintDebugString("product is auto renewing? " + info.isAutoRenewing());
    33.                             PrintDebugString("subscription remaining valid time until next billing date is: " + info.getRemainingTime());
    34.                             PrintDebugString("is this product in introductory price period? " + info.isIntroductoryPricePeriod());
    35.                             PrintDebugString("the product introductory localized price is: " + info.getIntroductoryPrice());
    36.                             PrintDebugString("the product introductory price period is: " + info.getIntroductoryPricePeriod());
    37.                             PrintDebugString("the number of product introductory price period cycles is: " + info.getIntroductoryPricePeriodCycles());
    38.                         }
    39.                         else
    40.                         {
    41.                             PrintDebugString("This product is not available for SubscriptionManager class, only products that are purchase by 1.19+ SDK can use this class.");
    42.                         }
    43.                     }
    44.                     else
    45.                     {
    46.                         PrintDebugString("the product is not a subscription product");
    47.                     }
    48.                 }
    49.                 else
    50.                 {
    51.                     PrintDebugString("the product should have a valid receipt");
    52.                 }
    53. #endif
     
  6. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    No, TestFlight is per user as expected, the user on your iPhone configured in TestFlight. What is the output of your debug statements? I don't believe you need the #if SUBSCRIPTION_MANAGER define. Please show the full code that you have now for ProcessPurchase (after testing and removing the #if define)
     
  7. Calamity_Jones

    Calamity_Jones

    Joined:
    Jul 31, 2019
    Posts:
    25
    I get the final message - the product should have a valid receipt.

    This is the full code - as I said, it's based on the IAPDemo - but I removed some stuff i didn't need and tried to tidy it up a little... I don't believe that I've removed anything important - just the UI stuff.


    Code (CSharp):
    1. #if UNITY_PURCHASING || UNITY_UNIFIED_IAP
    2.  
    3. #if UNITY_ANDROID || UNITY_IPHONE || UNITY_STANDALONE_OSX || UNITY_TVOS
    4. // You must obfuscate your secrets using Window > Unity IAP > Receipt Validation Obfuscator
    5. // before receipt validation will compile in this sample.
    6. //#define RECEIPT_VALIDATION
    7. #endif
    8.  
    9. //#define DELAY_CONFIRMATION // Returns PurchaseProcessingResult.Pending from ProcessPurchase, then calls ConfirmPendingPurchase after a delay
    10. //#define USE_PAYOUTS // Enables use of PayoutDefinitions to specify what the player should receive when a product is purchased
    11. //#define INTERCEPT_PROMOTIONAL_PURCHASES // Enables intercepting promotional purchases that come directly from the Apple App Store
    12. #define SUBSCRIPTION_MANAGER //Enables subscription product manager for AppleStore and GooglePlay store
    13. //#define AGGRESSIVE_INTERRUPT_RECOVERY_GOOGLEPLAY // Enables also using getPurchaseHistory to recover from purchase interruptions, assuming developer is deduplicating to protect against "duplicate on cancel" flow
    14. using System.Collections.Generic;
    15. using UnityEngine;
    16. using UnityEngine.Purchasing;
    17.  
    18. #if RECEIPT_VALIDATION
    19. using UnityEngine.Purchasing.Security;
    20. #endif
    21.  
    22. public class IAPManager : MonoBehaviour, IStoreListener
    23. {
    24.     // Unity IAP objects
    25.     private IStoreController m_Controller;
    26.  
    27.     private IAppleExtensions m_AppleExtensions;
    28.     private ISamsungAppsExtensions m_SamsungExtensions;
    29.     private IMicrosoftExtensions m_MicrosoftExtensions;
    30.     private ITransactionHistoryExtensions m_TransactionHistoryExtensions;
    31.     private IGooglePlayStoreExtensions m_GooglePlayStoreExtensions;
    32.  
    33. #pragma warning disable 0414
    34.     private bool m_IsGooglePlayStoreSelected;
    35. #pragma warning restore 0414
    36.     private bool m_IsSamsungAppsStoreSelected;
    37.  
    38.     private bool m_PurchaseInProgress;
    39.  
    40.     private Dictionary<string, IAPDemoProductUI> m_ProductUIs = new Dictionary<string, IAPDemoProductUI>();
    41.  
    42.     //public Button restoreButton;
    43.  
    44.     public bool IsUserPro = false;
    45.  
    46.     [Header("Debug")]
    47.     public bool PrintDebugData = true;
    48.  
    49. #if RECEIPT_VALIDATION
    50.     private CrossPlatformValidator validator;
    51. #endif
    52.  
    53.     // Prints a debug string if debugging is enabled.
    54.     private void PrintDebugString(string DebugText)
    55.     {
    56.         if (PrintDebugData == true)
    57.         {
    58.             Debug.Log(DebugText);
    59.         }
    60.     }
    61.  
    62.     // This will be called when Unity IAP has finished initialising.
    63.     public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    64.     {
    65.         m_Controller = controller;
    66.         m_AppleExtensions = extensions.GetExtension<IAppleExtensions>();
    67.         m_SamsungExtensions = extensions.GetExtension<ISamsungAppsExtensions>();
    68.         m_MicrosoftExtensions = extensions.GetExtension<IMicrosoftExtensions>();
    69.         m_TransactionHistoryExtensions = extensions.GetExtension<ITransactionHistoryExtensions>();
    70.         m_GooglePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();
    71.         // Sample code for expose product sku details for google play store Key is product Id (Sku), value is the skuDetails json string
    72.         //Dictionary<string, string> google_play_store_product_SKUDetails_json = m_GooglePlayStoreExtensions.GetProductJSONDictionary();
    73.         // Sample code for manually finish a transaction (consume a product on GooglePlay store)
    74.         //m_GooglePlayStoreExtensions.FinishAdditionalTransaction(productId, transactionId);
    75.  
    76.         // On Apple platforms we need to handle deferred purchases caused by Apple's Ask to Buy feature.
    77.         // On non-Apple platforms this will have no effect; OnDeferred will never be called.
    78.         m_AppleExtensions.RegisterPurchaseDeferredListener(OnDeferred);
    79.  
    80. #if SUBSCRIPTION_MANAGER
    81.         Dictionary<string, string> introductory_info_dict = m_AppleExtensions.GetIntroductoryPriceDictionary();
    82. #endif
    83.         // Sample code for expose product sku details for apple store
    84.         //Dictionary<string, string> product_details = m_AppleExtensions.GetProductDetails();
    85.  
    86.         PrintDebugString("Available items:");
    87.         foreach (var item in controller.products.all)
    88.         {
    89.             if (item.availableToPurchase)
    90.             {
    91.                 string DebugText = string.Join(" - ", new[]
    92.                     {
    93.                         item.metadata.localizedTitle,
    94.                         item.metadata.localizedDescription,
    95.                         item.metadata.isoCurrencyCode,
    96.                         item.metadata.localizedPrice.ToString(),
    97.                         item.metadata.localizedPriceString,
    98.                         item.transactionID,
    99.                         item.receipt
    100.                     });
    101.                 PrintDebugString(DebugText);
    102.  
    103. #if INTERCEPT_PROMOTIONAL_PURCHASES
    104.                 // Set all these products to be visible in the user's App Store according to Apple's Promotional IAP feature
    105.                 // https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/PromotingIn-AppPurchases/PromotingIn-AppPurchases.html
    106.                 m_AppleExtensions.SetStorePromotionVisibility(item, AppleStorePromotionVisibility.Show);
    107. #endif
    108.  
    109. #if SUBSCRIPTION_MANAGER
    110.                 // this is the usage of SubscriptionManager class
    111.                 if (item.receipt != null)
    112.                 {
    113.                     if (item.definition.type == ProductType.Subscription)
    114.                     {
    115.                         if (checkIfProductIsAvailableForSubscriptionManager(item.receipt))
    116.                         {
    117.                             string intro_json = (introductory_info_dict == null || !introductory_info_dict.ContainsKey(item.definition.storeSpecificId)) ? null : introductory_info_dict[item.definition.storeSpecificId];
    118.                             SubscriptionManager p = new SubscriptionManager(item, intro_json);
    119.                             SubscriptionInfo info = p.getSubscriptionInfo();
    120.  
    121.                             //--------------------------------------EME-------------------------------------
    122.                             if (info.isSubscribed() == Result.True)
    123.                             {
    124.                                 PrintDebugString("user is subscribed.");
    125.                                 GameController.Instance.SetUserPro(true);
    126.                                 IsUserPro = true;
    127.                             }
    128.                             else
    129.                             {
    130.                                 PrintDebugString("user is not subscribed.");
    131.                                 GameController.Instance.SetUserPro(false);
    132.                                 IsUserPro = false;
    133.                             }
    134.  
    135.                             PrintDebugString("product id is: " + info.getProductId());
    136.                             PrintDebugString("purchase date is: " + info.getPurchaseDate());
    137.                             PrintDebugString("subscription next billing date is: " + info.getExpireDate());
    138.                             PrintDebugString("is subscribed? " + info.isSubscribed().ToString());
    139.                             PrintDebugString("is expired? " + info.isExpired().ToString());
    140.                             PrintDebugString("is cancelled? " + info.isCancelled());
    141.                             PrintDebugString("product is in free trial peroid? " + info.isFreeTrial());
    142.                             PrintDebugString("product is auto renewing? " + info.isAutoRenewing());
    143.                             PrintDebugString("subscription remaining valid time until next billing date is: " + info.getRemainingTime());
    144.                             PrintDebugString("is this product in introductory price period? " + info.isIntroductoryPricePeriod());
    145.                             PrintDebugString("the product introductory localized price is: " + info.getIntroductoryPrice());
    146.                             PrintDebugString("the product introductory price period is: " + info.getIntroductoryPricePeriod());
    147.                             PrintDebugString("the number of product introductory price period cycles is: " + info.getIntroductoryPricePeriodCycles());
    148.                         }
    149.                         else
    150.                         {
    151.                             PrintDebugString("This product is not available for SubscriptionManager class, only products that are purchase by 1.19+ SDK can use this class.");
    152.                         }
    153.                     }
    154.                     else
    155.                     {
    156.                         PrintDebugString("the product is not a subscription product");
    157.                     }
    158.                 }
    159.                 else
    160.                 {
    161.                     PrintDebugString("the product should have a valid receipt");
    162.                 }
    163. #endif
    164.             }
    165.         }
    166.     }
    167.  
    168. #if SUBSCRIPTION_MANAGER
    169.     private bool checkIfProductIsAvailableForSubscriptionManager(string receipt)
    170.     {
    171.         var receipt_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(receipt);
    172.         if (!receipt_wrapper.ContainsKey("Store") || !receipt_wrapper.ContainsKey("Payload"))
    173.         {
    174.             PrintDebugString("The product receipt does not contain enough information");
    175.             return false;
    176.         }
    177.         var store = (string)receipt_wrapper ["Store"];
    178.         var payload = (string)receipt_wrapper ["Payload"];
    179.  
    180.         if (payload != null )
    181.         {
    182.             switch (store)
    183.             {
    184.                 case GooglePlay.Name:
    185.                 {
    186.                     var payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(payload);
    187.                     if (!payload_wrapper.ContainsKey("json"))
    188.                     {
    189.                         PrintDebugString("The product receipt does not contain enough information, the 'json' field is missing");
    190.                         return false;
    191.                     }
    192.                     var original_json_payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode((string)payload_wrapper["json"]);
    193.                     if (original_json_payload_wrapper == null || !original_json_payload_wrapper.ContainsKey("developerPayload"))
    194.                     {
    195.                         PrintDebugString("The product receipt does not contain enough information, the 'developerPayload' field is missing");
    196.                         return false;
    197.                     }
    198.                     var developerPayloadJSON = (string)original_json_payload_wrapper["developerPayload"];
    199.                     var developerPayload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(developerPayloadJSON);
    200.                     if (developerPayload_wrapper == null || !developerPayload_wrapper.ContainsKey("is_free_trial") || !developerPayload_wrapper.ContainsKey("has_introductory_price_trial"))
    201.                     {
    202.                         PrintDebugString("The product receipt does not contain enough information, the product is not purchased using 1.19 or later");
    203.                         return false;
    204.                     }
    205.                     return true;
    206.                 }
    207.                 case AppleAppStore.Name:
    208.                 case AmazonApps.Name:
    209.                 case MacAppStore.Name:
    210.                 {
    211.                     return true;
    212.                 }
    213.                 default:
    214.                 {
    215.                     return false;
    216.                 }
    217.             }
    218.         }
    219.         return false;
    220.     }
    221. #endif
    222.  
    223.     // --------------------------------------------------------------PURCHASE COMPLETE---------------------------------------------------------------
    224.     public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
    225.     {
    226.         PrintDebugString("Purchase OK: " + e.purchasedProduct.definition.id);
    227.         PrintDebugString("Receipt: " + e.purchasedProduct.receipt);
    228.  
    229.         GameController.Instance.HideLoadingIcon();
    230.         m_PurchaseInProgress = false;
    231.  
    232.         //--------------------------------------EME-------------------------------------
    233.  
    234.         int ProMonths = 0;
    235.         float ProPrice = 0;
    236.  
    237.         switch (e.purchasedProduct.definition.id)
    238.         {
    239.             case "eme.subscription.month.1":
    240.                 {
    241.                     ProMonths = 1;
    242.                     ProPrice = 4.99f;
    243.                 }
    244.                 break;
    245.             case "eme.subscription.month.12":
    246.                 {
    247.                     ProMonths = 12;
    248.                     ProPrice = 44.99f;
    249.                 }
    250.                 break;
    251.         }
    252.  
    253.         GameController.Instance.Service_Server.UserAccount_GetPro(ProMonths, ProPrice, e.purchasedProduct.definition.id, e.purchasedProduct.receipt);
    254.  
    255. #if RECEIPT_VALIDATION // Local validation is available for GooglePlay, and Apple stores
    256.         if (m_IsGooglePlayStoreSelected || Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.OSXPlayer || Application.platform == RuntimePlatform.tvOS)
    257.             {
    258.                 try
    259.                 {
    260.                 var result = validator.Validate(e.purchasedProduct.receipt);
    261.                 PrintDebugString("Receipt is valid. Contents:");
    262.                 foreach (IPurchaseReceipt productReceipt in result)
    263.                 {
    264.                     PrintDebugString(productReceipt.productID);
    265.                     PrintDebugString(productReceipt.purchaseDate);
    266.                     PrintDebugString(productReceipt.transactionID);
    267.  
    268.                     GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
    269.                     if (null != google)
    270.                     {
    271.                         PrintDebugString(google.purchaseState);
    272.                         PrintDebugString(google.purchaseToken);
    273.                     }
    274.  
    275.                     AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
    276.                     if (null != apple)
    277.                     {
    278.                         PrintDebugString(apple.originalTransactionIdentifier);
    279.                         PrintDebugString(apple.subscriptionExpirationDate);
    280.                         PrintDebugString(apple.cancellationDate);
    281.                         PrintDebugString(apple.quantity);
    282.                     }
    283.  
    284.                     // For improved security, consider comparing the signed IPurchaseReceipt.productId, IPurchaseReceipt.transactionID, and other data
    285.                     // embedded in the signed receipt objects to the data which the game is using to make this purchase.
    286.                 }
    287.             }
    288.             catch (IAPSecurityException ex)
    289.             {
    290.                 PrintDebugString("Invalid receipt, not unlocking content. " + ex);
    291.                 return PurchaseProcessingResult.Complete;
    292.             }
    293.         }
    294. #endif
    295.  
    296.         // Unlock content from purchases here.
    297. #if USE_PAYOUTS
    298.         if (e.purchasedProduct.definition.payouts != null)
    299.         {
    300.             PrintDebugString("Purchase complete, paying out based on defined payouts");
    301.             foreach (var payout in e.purchasedProduct.definition.payouts)
    302.             {
    303.                 PrintDebugString(string.Format("Granting {0} {1} {2} {3}", payout.quantity, payout.typeString, payout.subtype, payout.data));
    304.             }
    305.         }
    306. #endif
    307.         // Indicate if we have handled this purchase.
    308.         // PurchaseProcessingResult.Complete: ProcessPurchase will not be called with this product again, until next purchase. PurchaseProcessingResult.Pending: ProcessPurchase will be called again with this product at next app launch.
    309.         // Later, call m_Controller.ConfirmPendingPurchase(Product) to complete handling this purchase. Use to transactionally save purchases to a cloud game service.
    310. #if DELAY_CONFIRMATION
    311.         StartCoroutine(ConfirmPendingPurchaseAfterDelay(e.purchasedProduct));
    312.         return PurchaseProcessingResult.Pending;
    313. #else
    314.         return PurchaseProcessingResult.Complete;
    315. #endif
    316.     }
    317.  
    318. #if DELAY_CONFIRMATION
    319.     private HashSet<string> m_PendingProducts = new HashSet<string>();
    320.  
    321.     private IEnumerator ConfirmPendingPurchaseAfterDelay(Product p)
    322.     {
    323.         m_PendingProducts.Add(p.definition.id);
    324.         PrintDebugString("Delaying confirmation of " + p.definition.id + " for 5 seconds.");
    325.  
    326.         var end = Time.time + 5f;
    327.  
    328.         while (Time.time < end)
    329.         {
    330.             yield return null;
    331.             var remaining = Mathf.CeilToInt (end - Time.time);
    332.             UpdateProductPendingUI (p, remaining);
    333.         }
    334.  
    335.         if (m_IsGooglePlayStoreSelected)
    336.         {
    337.             PrintDebugString("Is " + p.definition.id + " currently owned, according to the Google Play store? " + m_GooglePlayStoreExtensions.IsOwned(p));
    338.         }
    339.         PrintDebugString("Confirming purchase of " + p.definition.id);
    340.         m_Controller.ConfirmPendingPurchase(p);
    341.         m_PendingProducts.Remove(p.definition.id);
    342.         UpdateProductUI (p);
    343.     }
    344. #endif
    345.  
    346.     // --------------------------------------------------------------PURCHASE FAILED---------------------------------------------------------------
    347.     public void OnPurchaseFailed(Product item, PurchaseFailureReason r)
    348.     {
    349.         PrintDebugString("Purchase failed: " + item.definition.id + " " + r.ToString());
    350.  
    351.         // Detailed debugging information
    352.         PrintDebugString("Store specific error code: " + m_TransactionHistoryExtensions.GetLastStoreSpecificPurchaseErrorCode());
    353.         if (m_TransactionHistoryExtensions.GetLastPurchaseFailureDescription() != null)
    354.         {
    355.             PrintDebugString("Purchase failure description message: " + m_TransactionHistoryExtensions.GetLastPurchaseFailureDescription().message);
    356.         }
    357.  
    358.         GameController.Instance.HideLoadingIcon();
    359.         m_PurchaseInProgress = false;
    360.     }
    361.  
    362.     public void OnInitializeFailed(InitializationFailureReason error)
    363.     {
    364.         PrintDebugString("Billing failed to initialize!");
    365.         switch (error)
    366.         {
    367.             case InitializationFailureReason.AppNotKnown:
    368.                 PrintDebugString("Is your App correctly uploaded on the relevant publisher console?");
    369.                 break;
    370.             case InitializationFailureReason.PurchasingUnavailable:
    371.                 // Ask the user if billing is disabled in device settings.
    372.                 PrintDebugString("Billing disabled!");
    373.                 break;
    374.             case InitializationFailureReason.NoProductsAvailable:
    375.                 // Developer configuration error; check product metadata.
    376.                 PrintDebugString("No products available for purchase!");
    377.                 break;
    378.         }
    379.     }
    380.  
    381.     public void Awake()
    382.     {
    383.         var module = StandardPurchasingModule.Instance();
    384.  
    385.         // The FakeStore supports: no-ui (always succeeding), basic ui (purchase pass/fail), and developer ui (initialization, purchase, failure code setting).
    386.         // These correspond to the FakeStoreUIMode Enum values passed into StandardPurchasingModule.useFakeStoreUIMode.
    387.         module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
    388.  
    389.         var builder = ConfigurationBuilder.Instance(module);
    390.  
    391.         // Set this to true to enable the Microsoft IAP simulator for local testing.
    392.         builder.Configure<IMicrosoftConfiguration>().useMockBillingSystem = false;
    393.  
    394.         m_IsGooglePlayStoreSelected =
    395.             Application.platform == RuntimePlatform.Android && module.appStore == AppStore.GooglePlay;
    396.  
    397. #if AGGRESSIVE_INTERRUPT_RECOVERY_GOOGLEPLAY
    398.         // For GooglePlay, if we have access to a backend server to deduplicate purchases, query purchase history when attempting to recover from a network-interruption encountered during purchasing. Strongly recommend
    399.         // deduplicating transactions across app reinstallations because this relies upon the on-device, deletable TransactionLog database.
    400.         builder.Configure<IGooglePlayConfiguration>().aggressivelyRecoverLostPurchases = true;
    401.         // Use purchaseToken instead of orderId for all transactions to avoid non-unique transactionIDs for a single purchase; two ProcessPurchase calls for one purchase, differing only by which field of the receipt
    402.         // is used for the Product.transactionID. Automatically true if aggressivelyRecoverLostPurchases is enabled and this API is not called at all.
    403.         builder.Configure<IGooglePlayConfiguration>().UsePurchaseTokenForTransactionId(true);
    404. #endif
    405.  
    406.         // Define our products. Either use the Unity IAP Catalog, or manually use the ConfigurationBuilder.AddProduct API.
    407.         // Use IDs from both the Unity IAP Catalog and hardcoded IDs via the ConfigurationBuilder.AddProduct API.
    408.  
    409.         // Use the products defined in the IAP Catalog GUI. E.g. Menu: "Window" > "Unity IAP" > "IAP Catalog", then add products, then click "App Store Export".
    410.         var catalog = ProductCatalog.LoadDefaultCatalog();
    411.  
    412.         foreach (var product in catalog.allValidProducts)
    413.         {
    414.             if (product.allStoreIDs.Count > 0)
    415.             {
    416.                 var ids = new IDs();
    417.                 foreach (var storeID in product.allStoreIDs)
    418.                 {
    419.                     ids.Add(storeID.id, storeID.store);
    420.                 }
    421.                 builder.AddProduct(product.id, product.type, ids);
    422.             }
    423.             else
    424.             {
    425.                 builder.AddProduct(product.id, product.type);
    426.             }
    427.         }
    428.  
    429.         // Write Amazon's JSON description of our products to storage when using Amazon's local sandbox. This should be removed from a production build.
    430.         //builder.Configure<IAmazonConfiguration>().WriteSandboxJSON(builder.products);
    431.  
    432.         // This enables simulated purchase success for Samsung IAP. You would remove this, or set to SamsungAppsMode.Production, before building your release package.
    433.         builder.Configure<ISamsungAppsConfiguration>().SetMode(SamsungAppsMode.AlwaysSucceed);
    434.         // This records whether we are using Samsung IAP. Currently ISamsungAppsExtensions.RestoreTransactions displays a blocking Android Activity, so:
    435.         // A) Unity IAP does not automatically restore purchases on Samsung Galaxy Apps
    436.         // B) IAPDemo (this) displays the "Restore" GUI button for Samsung Galaxy Apps
    437.         m_IsSamsungAppsStoreSelected = Application.platform == RuntimePlatform.Android && module.appStore == AppStore.SamsungApps;
    438.  
    439. #if INTERCEPT_PROMOTIONAL_PURCHASES
    440.         // On iOS and tvOS we can intercept promotional purchases that come directly from the App Store.
    441.         // On other platforms this will have no effect; OnPromotionalPurchase will never be called.
    442.         builder.Configure<IAppleConfiguration>().SetApplePromotionalPurchaseInterceptorCallback(OnPromotionalPurchase);
    443.         PrintDebugString("Setting Apple promotional purchase interceptor callback");
    444. #endif
    445.  
    446. #if RECEIPT_VALIDATION
    447.         string appIdentifier;
    448. #if UNITY_5_6_OR_NEWER
    449.         appIdentifier = Application.identifier;
    450. #else
    451.         appIdentifier = Application.bundleIdentifier;
    452. #endif
    453.         validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), appIdentifier);
    454. #endif
    455.  
    456.         // Now we're ready to initialize Unity IAP.
    457.         UnityPurchasing.Initialize(this, builder);
    458.     }
    459.  
    460.     // This will be called after a call to IAppleExtensions.RestoreTransactions().
    461.     private void OnTransactionsRestored(bool success)
    462.     {
    463.         PrintDebugString("Transactions restored." + success);
    464.  
    465.         GameController.Instance.HideLoadingIcon();
    466.         m_PurchaseInProgress = false;
    467.     }
    468.  
    469.     // iOS Specific. This is called as part of Apple's 'Ask to buy' functionality, when a purchase is requested by a minor and referred to a parent for approval.
    470.     // When the purchase is approved or rejected, the normal purchase events will fire.
    471.     // <param name="item">Item.</param>
    472.  
    473.     private void OnDeferred(Product item)
    474.     {
    475.         PrintDebugString("Purchase deferred: " + item.definition.id);
    476.     }
    477.  
    478. #if INTERCEPT_PROMOTIONAL_PURCHASES
    479.     private void OnPromotionalPurchase(Product item)
    480.     {
    481.         PrintDebugString("Attempted promotional purchase: " + item.definition.id);
    482.  
    483.         // Promotional purchase has been detected. Handle this event by, e.g. presenting a parental gate. Here, for demonstration purposes only, we will wait five seconds before continuing the purchase.
    484.         StartCoroutine(ContinuePromotionalPurchases());
    485.     }
    486.  
    487.     private IEnumerator ContinuePromotionalPurchases()
    488.     {
    489.         PrintDebugString("Continuing promotional purchases in 5 seconds");
    490.         yield return new WaitForSeconds(5);
    491.         PrintDebugString("Continuing promotional purchases now");
    492.         m_AppleExtensions.ContinuePromotionalPurchases (); // iOS and tvOS only; does nothing on Mac
    493.     }
    494. #endif
    495.  
    496.     public void PurchaseButtonClick(string productID)
    497.     {
    498.         if (m_PurchaseInProgress == true)
    499.         {
    500.             PrintDebugString("Please wait, purchase in progress");
    501.             return;
    502.         }
    503.  
    504.         if (m_Controller == null)
    505.         {
    506.             PrintDebugString("Purchasing is not initialized");
    507.             return;
    508.         }
    509.  
    510.         if (m_Controller.products.WithID(productID) == null)
    511.         {
    512.             PrintDebugString("No product has id " + productID);
    513.             return;
    514.         }
    515.  
    516.         GameController.Instance.ShowLoadingIcon();
    517.         m_PurchaseInProgress = true;
    518.  
    519.         //Sample code how to add accountId in developerPayload to pass it to getBuyIntentExtraParams
    520.         //Dictionary<string, string> payload_dictionary = new Dictionary<string, string>();
    521.         //payload_dictionary["accountId"] = "Faked account id";
    522.         //payload_dictionary["developerPayload"] = "Faked developer payload";
    523.         //m_Controller.InitiatePurchase(m_Controller.products.WithID(productID), MiniJson.JsonEncode(payload_dictionary));
    524.         m_Controller.InitiatePurchase(m_Controller.products.WithID(productID), "developerPayload");
    525.     }
    526.  
    527.     public void RestoreButtonClick()
    528.     {
    529.         GameController.Instance.ShowLoadingIcon();
    530.         m_PurchaseInProgress = true;
    531.  
    532.         if (m_IsSamsungAppsStoreSelected)
    533.         {
    534.             m_SamsungExtensions.RestoreTransactions(OnTransactionsRestored);
    535.         }
    536.         else if (m_IsGooglePlayStoreSelected)
    537.         {
    538.             m_GooglePlayStoreExtensions.RestoreTransactions(OnTransactionsRestored);
    539.         }
    540.         else
    541.         {
    542.             m_AppleExtensions.RestoreTransactions(OnTransactionsRestored);
    543.         }
    544.     }
    545. }
    546. #endif // UNITY_PURCHASING
     
  8. Calamity_Jones

    Calamity_Jones

    Joined:
    Jul 31, 2019
    Posts:
    25
    So if I understand correctly, the subscription process should be as follows:

    1. User buys a premium subscription. In ProcessPurchase, I parse the receipt data (billing date, issubscribed, etc), pass it to our server, and grant the user premium mode.
    2. The user uninstalls the app, then re-installs it while their subscription is still active. Either they click the restore button, or the restoration happens automatically (on Android). ProcessPurchase is called again, as though it were a new transaction, and again, I just parse the receipt data and pass it to the server.
    3. User closes the app, then re-opens it the next day or so. When starting up the app, OnInitialized is called, and I retrieve the receipt for their subscription purchase, and again, I parse the data and send it to the server.
    4. User cancels their subscription, and after a few weeks, logs in. This time, during OnInitialized I get iscancelled and isexpired == true, and I remove premium mode from their account.
    Is this correct?
     
  9. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    Please don't use the IAP Demo code, it will be rewritten soon. Start with the IAP Sample Project as I mentioned. Please use additional debugging. For example, what you just said implies that the product is not a subscription, then what type is it? When you write out your debug strings, also print out the variable values at the same time. Like this https://forum.unity.com/threads/tips-for-new-unity-users.701864/#post-5057741
     
  10. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    So you are performing receipt validation on your server? Otherwise all correct.
     
  11. Calamity_Jones

    Calamity_Jones

    Joined:
    Jul 31, 2019
    Posts:
    25
    Okay, so I feel like you're giving me conflicting instructions here.

    This thread: https://forum.unity.com/threads/improved-support-for-subscription-products.532811/

    Tells me to look at IAPDemo.cs for an example of subscription setup, but you're telling me to use the IAP Sample Project, which contains nothing for validating an existing subscription. There's bucketloads of very messy, ugly code in the IAPDemo.cs, which I'm very confused by.

    I've created a new "skeleton" for my IAP script using the script in the IAP Sample Project, which I've included below.

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Purchasing;
    3.  
    4. public class IAPManager2 : MonoBehaviour, IStoreListener
    5. {
    6.     private static IStoreController m_StoreController;          // The Unity Purchasing system.
    7.     private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
    8.  
    9.     [Header("Debug")]
    10.     public bool PrintDebugData = true;
    11.  
    12.     void Start()
    13.     {
    14.         // If we haven't set up the Unity Purchasing reference
    15.         if (m_StoreController == null)
    16.         {
    17.             // Begin to configure our connection to Purchasing
    18.             InitializePurchasing();
    19.         }
    20.     }
    21.  
    22.     // Initialize IAP.
    23.     public void InitializePurchasing()
    24.     {
    25.         if (IsInitialized())
    26.         {
    27.             return;
    28.         }
    29.  
    30.         var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    31.  
    32.         // Use the products defined in the IAP Catalog GUI. E.g. Menu: "Window" > "Unity IAP" > "IAP Catalog", then add products, then click "App Store Export".
    33.         var catalog = ProductCatalog.LoadDefaultCatalog();
    34.  
    35.         foreach (var product in catalog.allValidProducts)
    36.         {
    37.             if (product.allStoreIDs.Count > 0)
    38.             {
    39.                 var ids = new IDs();
    40.                 foreach (var storeID in product.allStoreIDs)
    41.                 {
    42.                     ids.Add(storeID.id, storeID.store);
    43.                 }
    44.                 builder.AddProduct(product.id, product.type, ids);
    45.             }
    46.             else
    47.             {
    48.                 builder.AddProduct(product.id, product.type);
    49.             }
    50.         }
    51.  
    52.         UnityPurchasing.Initialize(this, builder);
    53.     }
    54.  
    55.     // Check if initialized.
    56.     private bool IsInitialized()
    57.     {
    58.         return m_StoreController != null && m_StoreExtensionProvider != null;
    59.     }
    60.  
    61.     // Restore purchases.
    62.     public void RestorePurchases()
    63.     {
    64.         m_StoreExtensionProvider.GetExtension<IAppleExtensions>().RestoreTransactions(result => {
    65.             if (result)
    66.             {
    67.                 MyDebug("Restore purchases succeeded.");
    68.             }
    69.             else
    70.             {
    71.                 MyDebug("Restore purchases failed.");
    72.             }
    73.         });
    74.     }
    75.  
    76.     // Buy a product.
    77.     public void BuyProductID(string productId)
    78.     {
    79.         if (IsInitialized())
    80.         {
    81.             Product product = m_StoreController.products.WithID(productId);
    82.  
    83.             if (product != null && product.availableToPurchase)
    84.             {
    85.                 MyDebug(string.Format("Purchasing product:" + product.definition.id.ToString()));
    86.                 m_StoreController.InitiatePurchase(product);
    87.             }
    88.             else
    89.             {
    90.                 MyDebug("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
    91.             }
    92.         }
    93.         else
    94.         {
    95.             MyDebug("BuyProductID FAIL. Not initialized.");
    96.         }
    97.     }
    98.  
    99.     // initialization completed.
    100.     public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    101.     {
    102.         MyDebug("OnInitialized: PASS");
    103.  
    104.         m_StoreController = controller;
    105.         m_StoreExtensionProvider = extensions;
    106.  
    107.         // check if user has active subscription.
    108.         foreach (var item in controller.products.all)
    109.         {
    110.             if (item.receipt != null && item.definition.type == ProductType.Subscription)
    111.             {
    112.                 string intro_json = "???what is this???";
    113.                 SubscriptionManager p = new SubscriptionManager(item, intro_json);
    114.                 SubscriptionInfo info = p.getSubscriptionInfo();
    115.  
    116.                 if (info.isExpired() == Result.True)
    117.                 {
    118.                     // user is no longer subscribed.
    119.                 }
    120.                 else if (info.isSubscribed() == Result.True)
    121.                 {
    122.                     // user is subscribed.
    123.                 }
    124.             }
    125.         }
    126.     }
    127.  
    128.     // Initialization failed.
    129.     public void OnInitializeFailed(InitializationFailureReason error)
    130.     {
    131.         MyDebug("OnInitializeFailed InitializationFailureReason:" + error);
    132.     }
    133.  
    134.     // Purchase completed.
    135.     public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
    136.     {
    137.         Product PurchasedProduct = args.purchasedProduct;
    138.         MyDebug(string.Format("ProcessPurchase: Complete. Product:" + PurchasedProduct.definition.id + " - " + PurchasedProduct.transactionID.ToString()));
    139.  
    140.         // Pass data to server if purchase is subscription.
    141.         if(PurchasedProduct.definition.type == ProductType.Subscription)
    142.         {
    143.             string intro_json = "???what is this???";
    144.             SubscriptionManager p = new SubscriptionManager(PurchasedProduct, intro_json);
    145.             SubscriptionInfo info = p.getSubscriptionInfo();
    146.  
    147.             if (info.isExpired() == Result.True)
    148.             {
    149.                 // user is no longer subscribed.
    150.             }
    151.             else if (info.isSubscribed() == Result.True)
    152.             {
    153.                 // user is subscribed.
    154.             }
    155.         }
    156.  
    157.         return PurchaseProcessingResult.Complete;
    158.     }
    159.  
    160.     // Purchase failed.
    161.     public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    162.     {
    163.         MyDebug(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
    164.     }
    165.  
    166.     // Prints a debug string if debugging is enabled.
    167.     private void MyDebug(string debug)
    168.     {
    169.         if (PrintDebugData == true)
    170.         {
    171.             Debug.Log(debug);
    172.         }
    173.     }
    174. }
    How does this look? Are the basic components here?

    I appreciate the help so far!
     
    Last edited: Feb 1, 2021
  12. Calamity_Jones

    Calamity_Jones

    Joined:
    Jul 31, 2019
    Posts:
    25
    Regarding the creation of a SubscriptionManager, I don't understand what the Json object is, and the documentation merely tells me to refer to IAPDemo.cs, which you have told me not to use as a refrence.

    Code (CSharp):
    1.         string intro_json = (introductory_info_dict == null || !introductory_info_dict.ContainsKey(item.definition.storeSpecificId)) ? null : introductory_info_dict[item.definition.storeSpecificId];
    2.         SubscriptionManager p = new SubscriptionManager(item, intro_json);
    3.         SubscriptionInfo info = p.getSubscriptionInfo();
     
  13. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    Use the SubscriptionManager code in the Sample IAP Project. Sorry I wasn't more clear. The code you have above is correct. I would need to check on the intro_json use, I would just leave it as is for now. I do see that you are using store specific IDs, is there a specific reason? Just checking first, it's certainly legitimate. But I would get the exact sample project working first, create products gold50 and noads, etc. Then publish to Google and test. Then when that is working, add your customization. Publish to iOS after Google.
     
    Last edited: Feb 1, 2021
  14. Calamity_Jones

    Calamity_Jones

    Joined:
    Jul 31, 2019
    Posts:
    25
  15. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    Correct. Use the SubscriptionManager code that I referred to above, and place it in the Sample IAP Project.
     
  16. Calamity_Jones

    Calamity_Jones

    Joined:
    Jul 31, 2019
    Posts:
    25
    Oh, my apologies, do you mean the code here: https://forum.unity.com/threads/improved-support-for-subscription-products.532811/

    I copied over the code, could you let me know how this looks:

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3. using UnityEngine.Purchasing;
    4.  
    5. public class IAPManager2 : MonoBehaviour, IStoreListener
    6. {
    7.     private static IStoreController m_StoreController;          // The Unity Purchasing system.
    8.     private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
    9.     private IAppleExtensions m_AppleExtensions;
    10.  
    11.     [Header("Debug")]
    12.     public bool PrintDebugData = true;
    13.  
    14.     void Start()
    15.     {
    16.         // If we haven't set up the Unity Purchasing reference
    17.         if (m_StoreController == null)
    18.         {
    19.             // Begin to configure our connection to Purchasing
    20.             InitializePurchasing();
    21.         }
    22.     }
    23.  
    24.     // Initialize IAP.
    25.     public void InitializePurchasing()
    26.     {
    27.         if (IsInitialized())
    28.         {
    29.             return;
    30.         }
    31.  
    32.         var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    33.  
    34.         var catalog = ProductCatalog.LoadDefaultCatalog();
    35.  
    36.         foreach (var product in catalog.allValidProducts)
    37.         {
    38.             if (product.allStoreIDs.Count > 0)
    39.             {
    40.                 var ids = new IDs();
    41.                 foreach (var storeID in product.allStoreIDs)
    42.                 {
    43.                     ids.Add(storeID.id, storeID.store);
    44.                 }
    45.                 builder.AddProduct(product.id, product.type, ids);
    46.             }
    47.             else
    48.             {
    49.                 builder.AddProduct(product.id, product.type);
    50.             }
    51.         }
    52.  
    53.         UnityPurchasing.Initialize(this, builder);
    54.     }
    55.  
    56.     // Check if initialized.
    57.     private bool IsInitialized()
    58.     {
    59.         return m_StoreController != null && m_StoreExtensionProvider != null;
    60.     }
    61.  
    62.     // Restore purchases.
    63.     public void RestorePurchases()
    64.     {
    65.         m_StoreExtensionProvider.GetExtension<IAppleExtensions>().RestoreTransactions(result => {
    66.             if (result)
    67.             {
    68.                 MyDebug("Restore purchases succeeded.");
    69.             }
    70.             else
    71.             {
    72.                 MyDebug("Restore purchases failed.");
    73.             }
    74.         });
    75.     }
    76.  
    77.     // Buy a product.
    78.     public void BuyProductID(string productId)
    79.     {
    80.         if (IsInitialized())
    81.         {
    82.             Product product = m_StoreController.products.WithID(productId);
    83.  
    84.             if (product != null && product.availableToPurchase)
    85.             {
    86.                 MyDebug(string.Format("Purchasing product:" + product.definition.id.ToString()));
    87.                 m_StoreController.InitiatePurchase(product);
    88.             }
    89.             else
    90.             {
    91.                 MyDebug("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
    92.             }
    93.         }
    94.         else
    95.         {
    96.             MyDebug("BuyProductID FAIL. Not initialized.");
    97.         }
    98.     }
    99.  
    100.     // initialization completed.
    101.     public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    102.     {
    103.         MyDebug("OnInitialized: PASS");
    104.  
    105.         m_StoreController = controller;
    106.         m_StoreExtensionProvider = extensions;
    107.  
    108.         m_AppleExtensions = extensions.GetExtension<IAppleExtensions>();
    109.         Dictionary<string, string> dict = m_AppleExtensions.GetIntroductoryPriceDictionary();
    110.  
    111.         // check if user has active subscription.
    112.         foreach (var item in controller.products.all)
    113.         {
    114.             if (item.receipt != null)
    115.             {
    116.                 if(item.definition.type == ProductType.Subscription)
    117.                 {
    118.                     string intro_json = (dict == null || !dict.ContainsKey(item.definition.storeSpecificId)) ? null : dict[item.definition.storeSpecificId];
    119.                     SubscriptionManager p = new SubscriptionManager(item, intro_json);
    120.                     SubscriptionInfo info = p.getSubscriptionInfo();
    121.  
    122.                     if (info.isExpired() == Result.True)
    123.                     {
    124.                         // user is no longer subscribed.
    125.                     }
    126.                     else if (info.isSubscribed() == Result.True)
    127.                     {
    128.                         // user is subscribed.
    129.                     }
    130.                 }
    131.                 else
    132.                 {
    133.                     Debug.Log("the product is not a subscription product");
    134.                 }
    135.             }
    136.             else
    137.             {
    138.                 Debug.Log("the product should have a valid receipt");
    139.             }
    140.         }
    141.     }
    142.  
    143.     // Initialization failed.
    144.     public void OnInitializeFailed(InitializationFailureReason error)
    145.     {
    146.         MyDebug("OnInitializeFailed InitializationFailureReason:" + error);
    147.     }
    148.  
    149.     // Purchase completed.
    150.     public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
    151.     {
    152.         Product PurchasedProduct = args.purchasedProduct;
    153.         MyDebug(string.Format("ProcessPurchase: Complete. Product:" + PurchasedProduct.definition.id + " - " + PurchasedProduct.transactionID.ToString()));
    154.  
    155.         Dictionary<string, string> dict = m_AppleExtensions.GetIntroductoryPriceDictionary();
    156.  
    157.         if (PurchasedProduct.definition.type == ProductType.Subscription)
    158.         {
    159.             string intro_json = (dict == null || !dict.ContainsKey(PurchasedProduct.definition.storeSpecificId)) ? null : dict[PurchasedProduct.definition.storeSpecificId];
    160.             SubscriptionManager p = new SubscriptionManager(PurchasedProduct, intro_json);
    161.             SubscriptionInfo info = p.getSubscriptionInfo();
    162.  
    163.             if (info.isExpired() == Result.True)
    164.             {
    165.                 // user is no longer subscribed.
    166.             }
    167.             else if (info.isSubscribed() == Result.True)
    168.             {
    169.                 // user is subscribed.
    170.             }
    171.         }
    172.         else
    173.         {
    174.             Debug.Log("the product is not a subscription product");
    175.         }
    176.  
    177.         return PurchaseProcessingResult.Complete;
    178.     }
    179.  
    180.     // Purchase failed.
    181.     public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    182.     {
    183.         MyDebug(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
    184.     }
    185.  
    186.     // Prints a debug string if debugging is enabled.
    187.     private void MyDebug(string debug)
    188.     {
    189.         if (PrintDebugData == true)
    190.         {
    191.             Debug.Log(debug);
    192.         }
    193.     }
    194. }
     
  17. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    @Calamity_Jones I might recommend to leave the UI text element in the MyDebug, especially when deploying to a device during testing. Otherwise you'll need to monitor the device logs to debug. Otherwise, looks good. Except for your foreach loops, I would recommend to make as few changes as possible.
     
  18. Calamity_Jones

    Calamity_Jones

    Joined:
    Jul 31, 2019
    Posts:
    25
    Okay, thanks for your help! It's much appreciated :) I'll make a build for TestFlight and see what happens tomorrow.
     
  19. Calamity_Jones

    Calamity_Jones

    Joined:
    Jul 31, 2019
    Posts:
    25
    Hello, following some testing, we seem to have everything working now (thanks to your help!)

    I'm wondering how I would embed the username of the account that was logged in when a subscription is purchased - that would be using the payload, right?

    So would I embed the username as the payload, then retrieve this from the receipt to check that the logged in user, and the user account with the subscription match?
     
  20. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    No, you don't want capture any user info like that to maintain user privacy. The developer payload is no longer available, it was deprecated by the stores because developers were using it for this very purpose. There is no need to match a username with the subscription. The device security already does that, whomever is logged onto the device.
     
  21. Jeff_rey

    Jeff_rey

    Joined:
    Nov 26, 2017
    Posts:
    19
    @Calamity_Jones does your code above work for both ios and google? i see on line 109 that only apple is declared. Does Google require the same in an if else statement or switch statement, or am I missing something?

    @JeffDUnity3D


    I currently having it working with google and not apple and I was using the iapdemo for reference, but in that code it checks for the payload so I am uploading a new build now for google to see if I broke anything by commenting out that line of code. I'm currently trying to get the subscription to work for ios and it is not working currently.

    currently (and it is working for google) the code is as follows, but you said not to use payload. The code below
    Code (CSharp):
    1. if (product.definition.type == ProductType.Subscription)
    2.                     {
    3.                        
    4.                         if (CheckIfProductIsAvailableForSubscriptionManager(product.receipt))
    5.                         {
    6.                             string intro_json = (introductory_info_dict == null || !introductory_info_dict.ContainsKey(product.definition.storeSpecificId)) ? null : introductory_info_dict[product.definition.storeSpecificId];
    7.                             SubscriptionManager p = new SubscriptionManager(product, intro_json);
    8.                             SubscriptionInfo info = p.getSubscriptionInfo();
    9.                             if (info.isExpired() == Result.True)
    10.                             {
    11.                                 //subscription is not active
    12.                             }
    13.                             else if (info.isSubscribed() == Result.True)
    14.                             {
    15.                                 //subscription is active
    16.                             }
    17.                             Debug.Log("product id is: " + info.getProductId());
    18.                             Debug.Log("purchase date is: " + info.getPurchaseDate());
    19.                             Debug.Log("subscription next billing date is: " + info.getExpireDate());
    20.                             Debug.Log("is subscribed? " + info.isSubscribed().ToString());
    21.                             Debug.Log("is expired? " + info.isExpired().ToString());
    22.                             Debug.Log("is cancelled? " + info.isCancelled());
    23.                             Debug.Log("product is in free trial peroid? " + info.isFreeTrial());
    24.                             Debug.Log("product is auto renewing? " + info.isAutoRenewing());
    25.                             Debug.Log("subscription remaining valid time until next billing date is: " + info.getRemainingTime());
    26.                             Debug.Log("is this product in introductory price period? " + info.isIntroductoryPricePeriod());
    27.                             Debug.Log("the product introductory localized price is: " + info.getIntroductoryPrice());
    28.                             Debug.Log("the product introductory price period is: " + info.getIntroductoryPricePeriod());
    29.                             Debug.Log("the number of product introductory price period cycles is: " + info.getIntroductoryPricePeriodCycles());
    30.                          
    31.                        }
    32.                      
    33.                     }
    34.                     else
    35.                     {
    36.                         Debug.Log("the product is not a subscription product");
    37.                        if (product.definition.type == ProductType.NonConsumable)
    38.                         {
    39.                            if(product.definition.payouts!= null)
    40.                             {
    41.                                 foreach(var payout in product.definition.payouts)
    42.                                 {
    43.                                     string productID = product.definition.id;
    44.                                     switch (productID)
    45.                                     {
    46.                                      
    47.  
    48.  
    49.                                      
    50.                                     }
    51.                                 }
    52.                             }
    53.                         }
    54.  
    55.                     }
    56.                 }
    57.                 else
    58.                 {
    59.                     Debug.Log("the product should have a valid receipt");
    60.                     //subscription is not valid
    61.  
    62.                 }
    63.  
    64.             }
    65.  
    66.         }
    67.         LogProductDefinitions();
    68.     }
    69.  
    70.     private bool CheckIfProductIsAvailableForSubscriptionManager(string receipt)
    71.     {
    72.         var receipt_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(receipt);
    73.         if (!receipt_wrapper.ContainsKey("Store") || !receipt_wrapper.ContainsKey("Payload"))
    74.         {
    75.             Debug.Log("The product receipt does not contain enough information");
    76.          
    77.             return false;
    78.         }
    79.         var store = (string)receipt_wrapper["Store"];
    80.         var payload = (string)receipt_wrapper["Payload"];
    81.  
    82.         if (payload != null)
    83.         {
    84.             switch (store)
    85.             {
    86.                 case GooglePlay.Name:
    87.                     {
    88.                         var payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(payload);
    89.                         if (!payload_wrapper.ContainsKey("json"))
    90.                         {
    91.                             Debug.Log("The product receipt does not contain enough information, the 'json' field is missing");
    92.                          
    93.                             return false;
    94.                         }
    95.                         var original_json_payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode((string)payload_wrapper["json"]);
    96.                         if (original_json_payload_wrapper == null || !original_json_payload_wrapper.ContainsKey("developerPayload"))
    97.                         {
    98.                             Debug.Log("The product receipt does not contain enough information, the 'developerPayload' field is missing");
    99.                          
    100.                             return false;
    101.                         }
    102.                         var developerPayloadJSON = (string)original_json_payload_wrapper["developerPayload"];
    103.                         var developerPayload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(developerPayloadJSON);
    104.                         if (developerPayload_wrapper == null || !developerPayload_wrapper.ContainsKey("is_free_trial") || !developerPayload_wrapper.ContainsKey("has_introductory_price_trial"))
    105.                         {
    106.                             Debug.Log("The product receipt does not contain enough information, the product is not purchased using 1.19 or later");
    107.                          
    108.                             return false;
    109.                         }
    110.                      
    111.                         return true;
    112.                      
    113.  
    114.                     }
    115.                 case AppleAppStore.Name:
    116.                 case AmazonApps.Name:
    117.                 case MacAppStore.Name:
    118.                     {
    119.                      
    120.                         return true;
    121.                      
    122.                     }
    123.                 default:
    124.                     {
    125.                      
    126.                         return false;
    127.                     }
    128.             }
    129.         }
    130.      
    131.         return false;
    132.     }
    may have an extra space or an extra curly bracket as I have deleted some of my code to protect my info.
     
    Last edited: Feb 11, 2021
  22. Jeff_rey

    Jeff_rey

    Joined:
    Nov 26, 2017
    Posts:
    19
    in the demo on line 4 it checks line 70 but if I'm correct in what I read above I'm not supposed to use payloads (line 82). So am I correct in my understanding to omit the entire block of code on line 70 and line 4?
     
  23. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    The "developer payload" has been deprecated. Don't confuse that with the receipt as being a "payload". IAP Demo is outdated, I would not copy that code but instead do what has been suggested previously, add the subscription manager code to the Sample IAP Project. Don't use code in your program that you don't understand by simple copy and paste.
     
  24. Jeff_rey

    Jeff_rey

    Joined:
    Nov 26, 2017
    Posts:
    19
    @JeffDUnity3D

    It is really difficult to find a tangible reference and this post is the closest I've found to a potentially working version of the subscription Iap. There are no videos on my google searches that explain this fully. All of the videos are how to unlock coins and remove ads. Even the post with the sample project has a video and says in the next video I'll show how to use this project, but there is not a "next" video that I have been able to find. So I used the Iap demo as a reference, and in order to understand the code, I have to copy it and test it. It is working on google play still (just tested) but unsure if it will work for ios. Also, I only copied the subscription manager class from the IAP Demo and added it to my script. Where is the "subscription manager code" you said to add to the sample IAP project. I thought that was from the IAP Demo.

    Nevermind I see it above now. Thanks!
     
    Last edited: Feb 12, 2021
  25. Jeff_rey

    Jeff_rey

    Joined:
    Nov 26, 2017
    Posts:
    19
    I found my problem. I declared new ids for the different app stores:
    builder.AddProduct(productIDSubscription_AutoRenew, ProductType.Subscription, new IDs
    {

    {"com.blahblah.companyname.sub.auto", AppleAppStore.Name },
    {"com.blahblah.companyname.sub.automatic", GooglePlay.Name }
    });

    where it says AppleAppStore, I had MacAppStore. a simple overlooked mistake, don't make the same mistake I made.

    @Calamity_Jones Thanks for posting all of this, Seems like we were having the exact same problems, using the exact same references, and going down the same rabbit holes. Let me know if yours works and vice versa I'll do the same.
     
    Last edited: Feb 11, 2021
Thread Status:
Not open for further replies.