Search Unity

[Closed] How do I track auto-renewing subscriptions?

Discussion in 'Unity IAP' started by Deleted User, Jun 12, 2017.

Thread Status:
Not open for further replies.
  1. Deleted User

    Deleted User

    Guest

    I have auto-renewing subscriptions. My ultimate goal is to get a boolean that tells me whether or not the user has an active subscription. I have spent several days researching this, but have not found my answer.

    Some of the answers say that you must track the subscription yourself. Some of the answers say that the receipts contain the expiration date. Some of the answers say you cannot reliably use a receipt after the session it was purchased. Some of the answers are deprecated from the new Unity IAP system. Some of the answers...you get my point.

    I do not care about verifcation. If they want to cheat, so be it. I just want to know if they have an active subscription. I could record the first purchase and grant them a subscription, but what if they cancel their auto-renewing subscription? Basically, I have tried many different approaches and read hundreds of webpages. I am stuck, and there is no clear documentation. So my ultimate question:

    How can I check if the user has an active subscription? (Android and Apple)

    public bool IsSubscriptionActive()
    {
    //What goes here?
    }
     
    siddharth3322 likes this.
  2. ap-unity

    ap-unity

    Unity Technologies

    Joined:
    Aug 3, 2016
    Posts:
    1,519
    Yes, our documentation around subscriptions is a major blind spot right now, but we are working on improving that. We are also working on making some changes in the plugin itself to better support subscription products, which should improve this process as well.

    In the meantime, the best way to check for for a valid subscription is to manually parse the receipt.

    The receipt that is received from Unity IAP is a Unity-specific format that is JSON with three fields: Store, TransactionID, and Payload. The payload will contain the actual receipt as received from the app store, which is what you will need to parse to get the subscription info. (You can find the details of the payload in our Manual.)

    Once you have the receipt data, you can verify the subscription. The method for doing this will vary by platform. For example, Google Play has an autorenewing field in their receipt, while iOS has an expiration date on their receipt that will show you when the subscription expires.

    For iOS, all purchases will be part of the unified App Receipt (for iOS 7.0+). There is an example of how to parse this receipt in our Receipt Validation page:
    https://docs.unity3d.com/Manual/UnityIAPValidatingReceipts.html

    Code (CSharp):
    1. #if UNITY_IOS || UNITY_STANDALONE_OSX
    2. var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    3. // Get a reference to IAppleConfiguration during IAP initialization.
    4. var appleConfig = builder.Configure<IAppleConfiguration>();
    5. var receiptData = System.Convert.FromBase64String(appleConfig.appReceipt);
    6. AppleReceipt receipt = new AppleValidator(AppleTangle.Data()).Validate(receiptData);
    7.  
    8. Debug.Log(receipt.bundleID);
    9. Debug.Log(receipt.receiptCreationDate);
    10. foreach (AppleInAppPurchaseReceipt productReceipt in receipt.inAppPurchaseReceipts) {
    11.     Debug.Log(productReceipt.transactionIdentifier);
    12.     Debug.Log(productReceipt.productIdentifier);
    13. }
    14. #endif
    [Edit:]
    The above sample will run the receipt through the Validator, but if you don't want to do that, you can just parse the receipt:

    Code (CSharp):
    1. #if UNITY_IOS || UNITY_STANDALONE_OSX
    2. var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    3. // Get a reference to IAppleConfiguration during IAP initialization.
    4. var appleConfig = builder.Configure<IAppleConfiguration>();
    5. var receiptData = System.Convert.FromBase64String(appleConfig.appReceipt);
    6. AppleReceipt receipt = new AppleReceiptParser.Parse(receiptData);
    7.  
    8. Debug.Log(receipt.bundleID);
    9. Debug.Log(receipt.receiptCreationDate);
    10. foreach (AppleInAppPurchaseReceipt productReceipt in receipt.inAppPurchaseReceipts) {
    11.     Debug.Log(productReceipt.transactionIdentifier);
    12.     Debug.Log(productReceipt.productIdentifier);
    13. }
    14. #endif
    [/Edit]

    For Google Play, each product will have its own receipt. The json field in the payload of the Unity receipt will contain the INAPP_PURCHASE_DATA from Google:
    https://developer.android.com/google/play/billing/billing_reference.html#purchase-data-table

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. class GooglePurchaseData {
    4.     // INAPP_PURCHASE_DATA
    5.     public string inAppPurchaseData;
    6.     // INAPP_DATA_SIGNATURE
    7.     public string inAppDataSignature;
    8.  
    9.     public GooglePurchaseJson json;
    10.  
    11.     [System.Serializable]
    12.     private struct GooglePurchaseReceipt {
    13.         public string Payload;
    14.     }
    15.  
    16.     [System.Serializable]
    17.     private struct GooglePurchasePayload {
    18.         public string json;
    19.         public string signature;
    20.     }
    21.  
    22.     [System.Serializable]
    23.     public struct GooglePurchaseJson {
    24.         public string autoRenewing;
    25.         public string orderId;
    26.         public string packageName;
    27.         public string productId;
    28.         public string purchaseTime;
    29.         public string purchaseState;
    30.         public string developerPayload;
    31.         public string purchaseToken;
    32.     }
    33.  
    34.     public GooglePurchaseData(string receipt) {
    35.         try {
    36.             var purchaseReceipt = JsonUtility.FromJson<GooglePurchaseReceipt>(receipt);
    37.             var purchasePayload = JsonUtility.FromJson<GooglePurchasePayload>(purchaseReceipt.Payload);
    38.             var inAppJsonData = JsonUtility.FromJson<GooglePurchaseJson>(purchasePayload.json);
    39.  
    40.             inAppPurchaseData = purchasePayload.json;
    41.             inAppDataSignature = purchasePayload.signature;
    42.             json = inAppJsonData;
    43.         }
    44.         catch {
    45.             Debug.Log("Could not parse receipt: " + receipt);
    46.             inAppPurchaseData = "";
    47.             inAppDataSignature = "";
    48.         }
    49.     }
    50. }
    (I'm lifting most of this code from this discussion.)

    The usage is here:

    Code (CSharp):
    1. //This can be called anytime after initialization
    2. //And it should probably be limited to Google Play and not just Android
    3. #if UNITY_ANDROID
    4.         foreach (Product p in controller.products.all) {
    5.             GooglePurchaseData data = new GooglePurchaseData(p.receipt);
    6.  
    7.             if (p.hasReceipt) {
    8.                 Debug.Log(data.json.autoRenewing);
    9.                 Debug.Log(data.json.orderId);
    10.                 Debug.Log(data.json.packageName);
    11.                 Debug.Log(data.json.productId);
    12.                 Debug.Log(data.json.purchaseTime);
    13.                 Debug.Log(data.json.purchaseState);
    14.                 Debug.Log(data.json.purchaseToken);
    15.             }
    16.         }
    17. #endif
    As to your specific questions:

    This is correct in that Unity IAP does not have any inventory management features. So it would be best to track the state of a subscription in your own system and use the receipt to see if it is active.

    As I mentioned above, this varies by platform.

    For non-consumables and subscriptions, the receipt should be available. (Receipts for consumables are only available in the session that they were purchased.)

    The process for detecting active subscriptions will vary by platform, but saving the relevant data when the purchase is made and then parsing the receipt (during initialization or whenever necessary) should give you that information.

    Hopefully is this information is helpful.
     
    Last edited: Jul 14, 2017
  3. Deleted User

    Deleted User

    Guest

  4. dsmolczyk

    dsmolczyk

    Joined:
    Mar 23, 2017
    Posts:
    1
    Any chance you'd share how you ended up implementing IsSubscriptionActive()?
     
  5. acr1378

    acr1378

    Joined:
    Dec 4, 2014
    Posts:
    76
    I second that. @Evorlor did you manage to implement a IsSubscriptionActive() method?
     
  6. acr1378

    acr1378

    Joined:
    Dec 4, 2014
    Posts:
    76
    @ap-unity Does this mean that the receipt will be refreshed automatically when IAP is initialized? I've heard that's true for Android but not for Apple, but at this stage I'm getting so much conflicting information I don't know what to trust.


     
  7. acr1378

    acr1378

    Joined:
    Dec 4, 2014
    Posts:
    76
    Here's my first attempt. But not sure if it works. This assumes that we have a copy of the latest receipt, which is still a mystery to me how to get this in the case of auto-renewing subscriptions. I've made separate functions for Google and Apple receipts and combined them into one function that takes an IPurchaseReceipt as argument.

    Code (CSharp):
    1.    
    2.  
    3.     public bool isSubscriptionActive(IPurchaseReceipt productReceipt)
    4.     {
    5.         bool isActive = false;
    6.  
    7.         AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
    8.         GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
    9.         bool appleActive = isSubscriptionActive(apple);
    10.         bool googleActive = isSubscriptionActive(google);
    11.  
    12.         if(appleActive || googleActive)
    13.         {
    14.             isActive = true;
    15.         }
    16.            
    17.         return isActive;
    18.     }
    19.  
    20. public bool isSubscriptionActive(GooglePlayReceipt googleReceipt)
    21.     {
    22.         bool isActive = false;
    23.         GooglePlayReceipt google = googleReceipt;
    24.         if (null != google)
    25.         {
    26.             // Is this correct?
    27.             if(google.purchaseState == GooglePurchaseState.Purchased)
    28.             {
    29.                 isActive = true;
    30.             }
    31.         }
    32.  
    33.         return isActive;
    34.     }
    35.      
    36.  
    37.  
    38.     public bool isSubscriptionActive(AppleInAppPurchaseReceipt appleReceipt)
    39.     {
    40.         bool isActive = false;
    41.  
    42.         AppleInAppPurchaseReceipt apple = appleReceipt;
    43.         if (null != apple)
    44.         {
    45.             DateTime expirationDate = apple.subscriptionExpirationDate;
    46.             DateTime now = DateTime.Now;
    47.             //DateTime cancellationDate = apple.cancellationDate;
    48.  
    49.             if(DateTime.Compare(now, expirationDate) < 0)
    50.             {
    51.                 isActive = true;
    52.             }
    53.         }
    54.  
    55.         return isActive;
    56.     }
     
  8. amplifiedciaran

    amplifiedciaran

    Joined:
    Oct 24, 2016
    Posts:
    16
    Am I right in thinking that the receipt is autoupdated by their respective store, for instance, if a user cancels their subscription the autorenew field is changed on in the Android Payload? Also is the purchasedate changed upon a auto-renewal of the subscription?
     
  9. qVadro

    qVadro

    Joined:
    May 18, 2017
    Posts:
    24
    Why is it so complicated? :(
     
    Marcos-Elias and dinindu_unity like this.
  10. ThibaultGouala

    ThibaultGouala

    Joined:
    May 30, 2016
    Posts:
    12
    Hello,

    It seems so hard to get a clear answer about :
    - Can Unity IAP gets fresh receipts on IOS to check the status of a validation.

    Is there no justice in this world ? :(

    Would also be great to have more information for Google and Amazon as they are major platforms.
     
  11. NextTime

    NextTime

    Joined:
    Jan 10, 2017
    Posts:
    2



    --- it is for IAP iOS Subscription
    What to do if a user buys a subscription, turns off the Internet and every day changes the date in the opposite direction, how to check the subscription every time the game starts.
    because in result they play with subscription only free
     
  12. nicloay

    nicloay

    Joined:
    Jul 11, 2012
    Posts:
    540
    I tried compare subscriptionExpirationDate - but it's always the same. and lower than DatTime.Now even right after purchasing.
    Code (csharp):
    1. Debug.LogFormat(">>> {0}  {1}   {2}" , appleReceipt.subscriptionExpirationDate, appleReceipt.cancellationDate, DateTime.Now);
    gives me
    Code (csharp):
    1. >>> 11/27/2017 11:13:46  01/01/0001 00:00:00   11/27/2017 11:55:23
    This is auto renewable subscription. does this means that subscription is active if cancelationData == DateTime.minValue and i need to compare it's in validation process as well?



    update: Ok. I dig in to IAP code and found that expirationdate converted to the UTC format and I complare it with local datetime. @arandono In your code you have to change date comparison to this one
    Code (csharp):
    1.  
    2. if (appleReceipt.subscriptionExpirationDate > DateTime.Now.ToUniversalTime())
    3. {
    4.     return true; //HAS_ACTIVE_SUBSCRIPTION
    5. }
    6.  
     
    Last edited: Nov 27, 2017
    ismaelnascimentoash likes this.
  13. rattlesnake

    rattlesnake

    Joined:
    Jul 18, 2013
    Posts:
    138
    Thank you for this :)
    I am new to the IAP and I don't really know how to get the productReceipt. Do you have an exemple ? :)
    Thank you very much !
     
  14. rattlesnake

    rattlesnake

    Joined:
    Jul 18, 2013
    Posts:
    138
    I would like to test if a subscription is active at the app launch (when Initialized).
    I tried this (with a test buy account for Android) :

    Code (CSharp):
    1.     public bool checkIfProductBoughtV2(string productId)
    2.     {
    3.         if (m_StoreController != null)
    4.         {
    5.             // Fetch the currency Product reference from Unity Purchasing
    6.             Product product = m_StoreController.products.WithID(productId);
    7.      
    8.             var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
    9.          
    10.             var result = validator.Validate(product.receipt);
    11.      
    12.             /*
    13.             foreach (IPurchaseReceipt productReceipt in result)
    14.             {
    15.                 return isSubscriptionActive(productReceipt);
    16.             }
    17.             */
    18.             return isSubscriptionActive(result[0]);
    19.         }
    20.         return false;
    21.     }
    But I have a MissingStoreSecretException error :/

    Edit :
    And I I try :
    Code (CSharp):
    1.         foreach (Product p in m_StoreController.products.all) {
    2.             GooglePurchaseData data = new GooglePurchaseData(p.receipt);
    3.             if (p.hasReceipt)
    4.             {
    5.                 Debug.Log(data.json.autoRenewing);
    6.                 Debug.Log(data.json.orderId);
    7.                 Debug.Log(data.json.packageName);
    8.                 Debug.Log(data.json.productId);
    9.                 Debug.Log(data.json.purchaseTime);
    10.                 Debug.Log(data.json.purchaseState);
    11.                 Debug.Log(data.json.purchaseToken);
    12.             }
    13.         }
    purchaseState always return 0 :/

    Edit2 :
    The autoRenewing field seems to be a good marker. However I need to exit/launch the app in order to make it works
     
    Last edited: Dec 28, 2017
  15. Sir-Gatlin

    Sir-Gatlin

    Joined:
    Jan 18, 2013
    Posts:
    28
    IS there any more info on this front? I am setting up Auto renewal subscription and I see no info anywhere on the internet on how to make sure their auto sub stays active, or a way to check if apple updated the receipt without making the user sign in to get this info? So far I have the process of initializing purchaser > make purchase > save receipt locally > check it on every log in to make sure its still active. Is the general consensus of everyone in this thread that initializing will update the receipt when it is renewed?
     
  16. acr1378

    acr1378

    Joined:
    Dec 4, 2014
    Posts:
    76
    I had some issues with older version of Unity IAP (from a few months ago) where InitializePurchasing was not getting the latest receipt. It was (usually, but not always) calling ProcessPurchase automatically after running through its method, and ProcessPurchase was getting the mot recent receipt. That problem seems to be fixed on the latest version of Unity IAP. But be aware that InitializePurchasing still automatically calls ProcessPurchase so if you had custom code in there it will run at times that you don't expect it to run.
     
  17. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    On Android, it should work transparently. A renewed subscription should have the same transactionID as the original. If the subscription is expired or canceled, the product is dropped. On Apple, you'll need to parse the receipt each time. We are working to improve this behavior in a future release, I have requested to the IAP team to increase the priority of this task.
     
  18. Sir-Gatlin

    Sir-Gatlin

    Joined:
    Jan 18, 2013
    Posts:
    28
    Ok Jeff so you are saying that when I initialize and get a receipt back, it IS active, and if the subscription is not active then there will be no receipt?
     
  19. Sir-Gatlin

    Sir-Gatlin

    Joined:
    Jan 18, 2013
    Posts:
    28
    So I have been spending a lot of time getting my set up to work and want to share it so others don't have to go through the same lengths of finding answers as we all have had to do so far. To get everything working how I believe they should be working I needed two separate scripts. My purchaser, and a Google receipt parser. Will Post below. Please feel free to ask questions or improve upon:
    Code (CSharp):
    1.  
    2. using System;
    3. using UnityEngine;
    4. using UnityEngine.Purchasing;
    5. using UnityEngine.Purchasing.Security;
    6. // Deriving the Purchaser class from IStoreListener enables it to receive messages from Unity Purchasing.
    7. public class Purchaser : MonoBehaviour, IStoreListener
    8.     {
    9.         private static IStoreController m_StoreController;          // The Unity Purchasing system.
    10.         private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
    11.         // General product identifiers for the consumable, non-consumable, and subscription products.
    12.         // Use these handles in the code to reference which product to purchase. Also use these values
    13.         // when defining the Product Identifiers on the store. Except, for illustration purposes, the
    14.         // kProductIDSubscription - it has custom Apple and Google identifiers. We declare their store-
    15.         // specific mapping to Unity Purchasing's AddProduct, below.
    16.           public static string kProductIDMonth = "MonthSubscription";
    17.         public static string kProductIDYear = "YearSubscription";
    18.         // Apple App Store-specific product identifier for the subscription product.
    19.         private static string kProductNameAppleMonth = "com.company.app.monthsub";
    20.         private static string kProductNameAppleYear = "com.company.app.yearsub";
    21.         // Google Play Store-specific product identifier subscription product.
    22.         private static string kProductNameGoogleMonth = "com.company.app.monthsub";
    23.         private static string kProductNameGoogleYear = "com.company.app.yearsub";
    24.         // Does the math to see if your apple subscription is past its experation date
    25.         public bool IsSubActive(AppleInAppPurchaseReceipt e)
    26.         {
    27.             if (e.subscriptionExpirationDate > DateTime.Now.ToUniversalTime())
    28.             {
    29.                 return true; //HAS_ACTIVE_SUBSCRIPTION
    30.             }
    31.             else
    32.             {
    33.                 return false;
    34.             }
    35.         }
    36.         public BadgeCollection Badge;
    37.         public db_connect db_connect;
    38.     void Start()
    39.         {
    40.             // If we haven't set up the Unity Purchasing reference
    41.             if (m_StoreController == null)
    42.             {
    43.                 // Begin to configure our connection to Purchasing
    44.                 InitializePurchasing();
    45.             }
    46.         }
    47.         public void InitializePurchasing()
    48.         {
    49.             // If we have already connected to Purchasing ...
    50.             if (IsInitialized())
    51.             {
    52.                 // ... we are done here.
    53.                 return;
    54.             }
    55.             // Create a builder, first passing in a suite of Unity provided stores.
    56.             var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    57.             // Add a product to sell / restore by way of its identifier, associating the general identifier
    58.             // with its store-specific identifiers.
    59.             // And finish adding the subscription product. Notice this uses store-specific IDs, illustrating
    60.             // if the Product ID was configured differently between Apple and Google stores. Also note that
    61.             // one uses the general kProductIDSubscription handle inside the game - the store-specific IDs
    62.             // must only be referenced here.
    63.             builder.AddProduct(kProductIDMonth, ProductType.Subscription, new IDs(){
    64.             { kProductNameAppleMonth, AppleAppStore.Name},
    65.             { kProductNameGoogleMonth, GooglePlay.Name},
    66.             });
    67.             builder.AddProduct(kProductIDYear, ProductType.Subscription, new IDs(){
    68.             { kProductNameAppleYear, AppleAppStore.Name },
    69.             { kProductNameGoogleYear, GooglePlay.Name },
    70.             });
    71.             Debug.Log ("add products done");
    72.             // Kick off the remainder of the set-up with an asynchrounous call, passing the configuration
    73.             // and this class' instance. Expect a response either in OnInitialized or OnInitializeFailed.
    74.             UnityPurchasing.Initialize(this, builder);
    75.         }
    76.         private bool IsInitialized()
    77.         {
    78.             // Only say we are initialized if both the Purchasing references are set.
    79.             return m_StoreController != null && m_StoreExtensionProvider != null;
    80.  
    81.         }
    82.         public void BuyMonth()
    83.         {
    84.             // Buy the subscription product using its the general identifier. Expect a response either
    85.             // through ProcessPurchase or OnPurchaseFailed asynchronously.
    86.             // Notice how we use the general product identifier in spite of this ID being mapped to
    87.             // custom store-specific identifiers above.
    88.             BuyProductID(kProductIDMonth);
    89.         }
    90.         public void BuyYear()
    91.             {
    92.                 // Buy the subscription product using its the general identifier. Expect a response either
    93.                 // through ProcessPurchase or OnPurchaseFailed asynchronously.
    94.                 // Notice how we use the general product identifier in spite of this ID being mapped to
    95.                 // custom store-specific identifiers above.
    96.                 BuyProductID(kProductIDYear);
    97.             }
    98.         void BuyProductID(string productId)
    99.             {
    100.                 // If Purchasing has been initialized ...
    101.                 if (IsInitialized())
    102.                 {
    103.                     // ... look up the Product reference with the general product identifier and the Purchasing
    104.                     // system's products collection.
    105.                     Product product = m_StoreController.products.WithID(productId);
    106.                     // If the look up found a product for this device's store and that product is ready to be sold ...
    107.                     if (product != null && product.availableToPurchase)
    108.                     {
    109.                         Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));
    110.                         // ... buy the product. Expect a response either through ProcessPurchase or OnPurchaseFailed
    111.                         // asynchronously.
    112.                         m_StoreController.InitiatePurchase(product);
    113.                     }
    114.                     // Otherwise ...
    115.                     else
    116.                     {
    117.                         // ... report the product look-up failure situation
    118.                         Debug.Log("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
    119.                     }
    120.                 }
    121.                 // Otherwise ...
    122.                 else
    123.                 {
    124.                     // ... report the fact Purchasing has not succeeded initializing yet. Consider waiting longer or
    125.                     // retrying initiailization.
    126.                     Debug.Log("BuyProductID FAIL. Not initialized.");
    127.                 }
    128.             }
    129.         // Restore purchases previously made by this customer. Some platforms automatically restore purchases, like Google.
    130.         // Apple currently requires explicit purchase restoration for IAP, conditionally displaying a password prompt.
    131.         public void RestorePurchases()
    132.         {
    133.             // If Purchasing has not yet been set up ...
    134.             if (!IsInitialized())
    135.             {
    136.                 // ... report the situation and stop restoring. Consider either waiting longer, or retrying initialization.
    137.                 Debug.Log("RestorePurchases FAIL. Not initialized.");
    138.                 return;
    139.             }
    140.             // If we are running on an Apple device ...
    141.             if (Application.platform == RuntimePlatform.IPhonePlayer ||
    142.                 Application.platform == RuntimePlatform.OSXPlayer)
    143.             {
    144.                 // ... begin restoring purchases
    145.                 Debug.Log("RestorePurchases started ...");
    146.                 // Fetch the Apple store-specific subsystem.
    147.                 var apple = m_StoreExtensionProvider.GetExtension<IAppleExtensions>();
    148.                 // Begin the asynchronous process of restoring purchases. Expect a confirmation response in
    149.                 // the Action<bool> below, and ProcessPurchase if there are previously purchased products to restore.
    150.                 apple.RestoreTransactions((result) => {
    151.                     // The first phase of restoration. If no more responses are received on ProcessPurchase then
    152.                     // no purchases are available to be restored.
    153.                     Debug.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
    154.                 });
    155.             }
    156.             // Otherwise ...
    157.             else
    158.             {
    159.                 // We are not running on an Apple device. No work is necessary to restore purchases.
    160.                 Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
    161.             }
    162.         }
    163.         public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    164.         {
    165.             // Purchasing has succeeded initializing. Collect our Purchasing references.
    166.             Debug.Log("OnInitialized: PASS");
    167.          
    168.             // Overall Purchasing system, configured with products for this application.
    169.             m_StoreController = controller;
    170.             // Store specific subsystem, for accessing device-specific store features.
    171.             m_StoreExtensionProvider = extensions;
    172.         //This can be called anytime after initialization
    173.         //And it should probably be limited to Google Play and not just Android
    174. #if UNITY_ANDROID
    175.         foreach (Product p in controller.products.all)
    176.         {
    177.             // Refering to the extra GooglePurchaseData class provided
    178.             GooglePurchaseData data = new GooglePurchaseData(p.receipt);
    179.             if (p.hasReceipt)
    180.             {
    181.                 // Allows you to easily refer to data from the receipt for the subscription,
    182.                 // if AutoRenewing is true, then their subscrition is active.
    183.                 Debug.Log("autoRenewing: "+data.json.autoRenewing);
    184.                 if (data.json.autoRenewing == "true")
    185.                 {
    186.                     Data.isSubscriber = true;
    187.                     Badge.init();
    188.                     db_connect.ClosePaywall();
    189.                 }
    190.                 Debug.Log(data.json.orderId);
    191.                 Debug.Log(data.json.packageName);
    192.                 Debug.Log(data.json.productId);
    193.                 Debug.Log(data.json.purchaseTime);
    194.                 Debug.Log(data.json.purchaseState);
    195.                 Debug.Log(data.json.purchaseToken);
    196.             }
    197.         }
    198. #endif
    199. #if UNITY_IOS
    200.         // Apple will only update your receipt when it is changed, until then you will have
    201.         // to save the last one provided locally ot be able to track the subscription yourself
    202.         string localsave = PlayerPrefs.GetString ("Receipt", null);
    203.         // I store the data locally Obfuscated to make it harder to cheat
    204.         // so I must call the validator to made the Receipt readable.
    205.         // remember you must run the Obfuscater in the Unity IAP window to create the Tangle files
    206.         var validator = new CrossPlatformValidator(GooglePlayTangle.Data(),
    207.             AppleTangle.Data(), Application.identifier);
    208.         var localResult = validator.Validate (localsave);
    209.         Debug.Log ("Local Receipt: " + localResult);
    210.         foreach (IPurchaseReceipt productReceipt in localResult) {
    211.             Debug.Log ("IsInitialized local data");
    212.             Debug.Log(productReceipt.productID);
    213.             Debug.Log(productReceipt.purchaseDate);
    214.             Debug.Log(productReceipt.transactionID);
    215.             AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
    216.             if (null != apple) {
    217.                 Debug.Log ("On Initialized Apple:");
    218.                 Debug.Log("TransactionID: "+apple.originalTransactionIdentifier);
    219.                 Debug.Log("ExpirationDate: "+apple.subscriptionExpirationDate);
    220.                 Debug.Log("Purchase Date: "+apple.purchaseDate);
    221.                 // runs the experation time compared to the current time
    222.                 if (IsSubActive (apple)) {
    223.                     Debug.Log ("Sub is Active");
    224.                     Data.isSubscriber = true;
    225.                     Badge.init();
    226.                     db_connect.ClosePaywall();
    227.                 } else {
    228.                     Debug.Log ("Sub is NOT Active");
    229.                 }
    230.             }
    231.         }
    232. #endif
    233.     }
    234.     public void OnInitializeFailed(InitializationFailureReason error)
    235.         {
    236.             // Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
    237.             Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
    238.         }
    239.         public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
    240.         {
    241.          
    242.             // Or ... a subscription product has been purchased by this user.
    243.            if (String.Equals(args.purchasedProduct.definition.id, kProductIDMonth, StringComparison.Ordinal))
    244.             {
    245.                 Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
    246.                 Debug.Log("save reciept and set expiration date");
    247.                 PlayerPrefs.SetString("Receipt", args.purchasedProduct.receipt);
    248.                 PlayerPrefs.Save();
    249.                 // Prepare the validator with the secrets we prepared in the Editor
    250.                 // obfuscation window.
    251.                 var validator = new CrossPlatformValidator(GooglePlayTangle.Data(),
    252.                 AppleTangle.Data(), Application.identifier);
    253.                 var result = validator.Validate(args.purchasedProduct.receipt);
    254.              
    255.                 // For informational purposes, we list the receipt(s)
    256.                 Debug.Log("Receipt is valid.");
    257.                 foreach (IPurchaseReceipt productReceipt in result) {
    258.                     Debug.Log(productReceipt.productID);
    259.                     Debug.Log(productReceipt.purchaseDate);
    260.                     Debug.Log(productReceipt.transactionID);
    261.                     GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
    262.                     if (null != google) {
    263.                         Debug.Log ("Google");
    264.                         Debug.Log(google.ToString());
    265.                         Debug.Log(google.purchaseToken);
    266.                     }
    267.                     AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
    268.                     if (null != apple) {
    269.                         Debug.Log ("ProcessPurchase Apple:");
    270.                         Debug.Log(apple.originalTransactionIdentifier);
    271.                         Debug.Log(apple.subscriptionExpirationDate);
    272.                         if (IsSubActive (apple)) {
    273.                             Debug.Log ("Sub is Active");
    274.                             Data.isSubscriber = true;
    275.                             db_connect.ClosePaywall();
    276.                     } else {
    277.                             Debug.Log ("Sub is NOT Active");
    278.                         }
    279.                     }
    280.                 }
    281.             Debug.Log("Unlock Subscription for month");
    282.                 Data.isSubscriber = true;
    283.                 Badge.init();
    284.         }
    285.             // Or ... a Year subscription product has been purchased by this user.
    286.             else if (String.Equals(args.purchasedProduct.definition.id, kProductIDYear, StringComparison.Ordinal))
    287.             {
    288.                 Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
    289.                 Debug.Log("save reciept and set expiration date");
    290.                 PlayerPrefs.SetString("Receipt", args.purchasedProduct.receipt);
    291.                 PlayerPrefs.Save();
    292.                 Data.isSubscriber = true;
    293.                 Badge.init();
    294.                 db_connect.ClosePaywall();
    295.         }
    296.             // Or ... an unknown product has been purchased by this user. Fill in additional products here....
    297.             else
    298.             {
    299.                 Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
    300.             }
    301.             // Return a flag indicating whether this product has completely been received, or if the application needs
    302.             // to be reminded of this purchase at next app launch. Use PurchaseProcessingResult.Pending when still
    303.             // saving purchased products to the cloud, and when that save is delayed.
    304.             return PurchaseProcessingResult.Complete;
    305.         }
    306.         public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    307.         {
    308.             // A product purchase attempt did not succeed. Check failureReason for more detail. Consider sharing
    309.             // this reason with the user to guide their troubleshooting actions.
    310.             Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
    311.         }
    312.     }
    313.  
    Then I have the GooglePurchaseData, this parses the google receipt in order to get to the autoRenewal string:
    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. class GooglePurchaseData
    4. {
    5.     // INAPP_PURCHASE_DATA
    6.     public string inAppPurchaseData;
    7.     // INAPP_DATA_SIGNATURE
    8.     public string inAppDataSignature;
    9.     public GooglePurchaseJson json;
    10.     [System.Serializable]
    11.     private struct GooglePurchaseReceipt
    12.     {
    13.         public string Payload;
    14.     }
    15.     [System.Serializable]
    16.     private struct GooglePurchasePayload
    17.     {
    18.         public string json;
    19.         public string signature;
    20.     }
    21.     [System.Serializable]
    22.     public struct GooglePurchaseJson
    23.     {
    24.         public string autoRenewing;
    25.         public string orderId;
    26.         public string packageName;
    27.         public string productId;
    28.         public string purchaseTime;
    29.         public string purchaseState;
    30.         public string developerPayload;
    31.         public string purchaseToken;
    32.     }
    33.     public GooglePurchaseData(string receipt)
    34.     {
    35.         try
    36.         {
    37.             var purchaseReceipt = JsonUtility.FromJson<GooglePurchaseReceipt>(receipt);
    38.             var purchasePayload = JsonUtility.FromJson<GooglePurchasePayload>(purchaseReceipt.Payload);
    39.             var inAppJsonData = JsonUtility.FromJson<GooglePurchaseJson>(purchasePayload.json);
    40.             inAppPurchaseData = purchasePayload.json;
    41.             inAppDataSignature = purchasePayload.signature;
    42.             json = inAppJsonData;
    43.         }
    44.         catch
    45.         {
    46.             Debug.Log("Could not parse receipt: " + receipt);
    47.             inAppPurchaseData = "";
    48.             inAppDataSignature = "";
    49.         }
    50.     }
    51. }
    52.  
    So what I have learned is that Google will give you a receipt every single time the purchaser is initialized, If what I am being told is correct, you don't need to parse the data and check if autoRenewal is set to True, as if it was canceled there would be no receipt received anyways. Where as with Apple, It will only give you a new receipt if it has been renewed. so you will have to store the receipt locally to check if they are still subscribed, and replace that receipt every time a new one is given to you.
    Anyone see anywhere that I am wrong? or have improvements on my code please share! :D
     
    Last edited: Jan 17, 2018
    rmjfox and dappledore like this.
  20. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    @Sir-Gatlin Please test your code, but your statements are correct. Keep in mind that if a user deletes and re-installs your game, the PlayerPrefs would be reset, so something to test also.
     
    ismaelnascimentoash likes this.
  21. Sir-Gatlin

    Sir-Gatlin

    Joined:
    Jan 18, 2013
    Posts:
    28
    yeah, I guess I was assuming that is what Restore purchases are for, is that correct or is there a better way to renew it for them?
     
  22. Sir-Gatlin

    Sir-Gatlin

    Joined:
    Jan 18, 2013
    Posts:
    28
    So, I have learned that when an Android user cancels their subscription autoRenewing turns to false. Which kind of makes sense, but reading on the android docs lead me to believe this told us if it was active or not. So the best way to test if a user has a active subscription is what @JeffDUnity3D explain, that if there is a receipt, it is active. I will fix my script and update my Post above.
     
  23. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    Restore purchases will let you (the developer) know which products need to be restored. It is up to you to actually provide the product to the customer (unlock the level, etc)
     
  24. Sir-Gatlin

    Sir-Gatlin

    Joined:
    Jan 18, 2013
    Posts:
    28
    @JeffDUnity3D oh, I guess I misread the Docs. under Restoring Transactions it mentions that it will run ProcessPurchase for all items that have been restored, I figured that meant it would run my code I put in ProcessPurchase, which Unlocks the content. Do the Docs need to be updated or am I reading it wrong? Screenshot (74).png
     
  25. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    @Sir-Gatlin I had stated "It is up to you to actually provide the product to the customer (unlock the level, etc)" with your reply "that meant it would run my code I put in ProcessPurchase, which Unlocks the content". We are saying the same thing. We don't unlock the products for you was my point, you need to do that yourself, which you are already doing. So your understanding is correct, and as I stated also. I hope this clears things up.
     
    Sir-Gatlin likes this.
  26. Sir-Gatlin

    Sir-Gatlin

    Joined:
    Jan 18, 2013
    Posts:
    28
    Yes it does, thank you for the quick response and clarification.
     
  27. Sir-Gatlin

    Sir-Gatlin

    Joined:
    Jan 18, 2013
    Posts:
    28
    here is my final Purchase script. there is no longer need for the extra script to parse google receipts since you only have to make sure there is a receipt and not have to read it
    Code (CSharp):
    1.  
    2. using System;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.Purchasing;
    6. using UnityEngine.Purchasing.Security;
    7. // Deriving the Purchaser class from IStoreListener enables it to receive messages from Unity Purchasing.
    8. public class Purchaser : MonoBehaviour, IStoreListener
    9.     {
    10.         private static IStoreController m_StoreController;          // The Unity Purchasing system.
    11.         private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
    12.         // General product identifiers for the consumable, non-consumable, and subscription products.
    13.         // Use these handles in the code to reference which product to purchase. Also use these values
    14.         // when defining the Product Identifiers on the store. Except, for illustration purposes, the
    15.         // kProductIDSubscription - it has custom Apple and Google identifiers. We declare their store-
    16.         // specific mapping to Unity Purchasing's AddProduct, below.
    17.           public static string kProductIDMonth = "MonthSubscription";
    18.         public static string kProductIDYear = "YearSubscription";
    19.         // Apple App Store-specific product identifier for the subscription product.
    20.         private static string kProductNameAppleMonth = "com.company.app.monthsub";
    21.         private static string kProductNameAppleYear = "com.company.app.yearsub";
    22.         // Google Play Store-specific product identifier subscription product.
    23.         private static string kProductNameGoogleMonth = "com.company.app.monthsub";
    24.         private static string kProductNameGoogleYear = "com.company.app.yearsub";
    25.         // Does the math to see if your apple subscription is past its experation date  
    26.         public bool IsSubActive(AppleInAppPurchaseReceipt e)
    27.         {
    28.             if (e.subscriptionExpirationDate > DateTime.Now.ToUniversalTime())
    29.             {
    30.                 return true; //HAS_ACTIVE_SUBSCRIPTION
    31.             }
    32.             else
    33.             {
    34.                 return false;
    35.             }
    36.         }
    37.         public BadgeCollection Badge;
    38.         public db_connect db_connect;
    39.         static List<string> DownloadedVideos;
    40.     void Start()
    41.         {
    42.             // If we haven't set up the Unity Purchasing reference
    43.             if (m_StoreController == null)
    44.             {
    45.                 // Begin to configure our connection to Purchasing
    46.                 InitializePurchasing();
    47.             }
    48.             Badge.init();
    49.         Data.DownloadedVideos = new List<string>();
    50.         Data.DownloadedVideos.Add("World");
    51.         if (Data.DownloadedVideos.Contains("World"))
    52.         {
    53.             Debug.Log("does exist in list");
    54.         }
    55.     }
    56.         public void InitializePurchasing()
    57.         {
    58.             // If we have already connected to Purchasing ...
    59.             if (IsInitialized())
    60.             {
    61.                 // ... we are done here.
    62.                 return;
    63.             }
    64.             // Create a builder, first passing in a suite of Unity provided stores.
    65.             var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    66.             // Add a product to sell / restore by way of its identifier, associating the general identifier
    67.             // with its store-specific identifiers.
    68.             // And finish adding the subscription product. Notice this uses store-specific IDs, illustrating
    69.             // if the Product ID was configured differently between Apple and Google stores. Also note that
    70.             // one uses the general kProductIDSubscription handle inside the game - the store-specific IDs
    71.             // must only be referenced here.
    72.             builder.AddProduct(kProductIDMonth, ProductType.Subscription, new IDs(){
    73.             { kProductNameAppleMonth, AppleAppStore.Name},
    74.             { kProductNameGoogleMonth, GooglePlay.Name},
    75.             });
    76.             builder.AddProduct(kProductIDYear, ProductType.Subscription, new IDs(){
    77.             { kProductNameAppleYear, AppleAppStore.Name },
    78.             { kProductNameGoogleYear, GooglePlay.Name },
    79.             });
    80.             Debug.Log ("add products done");
    81.             // Kick off the remainder of the set-up with an asynchrounous call, passing the configuration
    82.             // and this class' instance. Expect a response either in OnInitialized or OnInitializeFailed.
    83.             UnityPurchasing.Initialize(this, builder);
    84.         }
    85.         private bool IsInitialized()
    86.         {
    87.             // Only say we are initialized if both the Purchasing references are set.
    88.             return m_StoreController != null && m_StoreExtensionProvider != null;
    89.    
    90.         }
    91.         public void BuyMonth()
    92.         {
    93.             // Buy the subscription product using its the general identifier. Expect a response either
    94.             // through ProcessPurchase or OnPurchaseFailed asynchronously.
    95.             // Notice how we use the general product identifier in spite of this ID being mapped to
    96.             // custom store-specific identifiers above.
    97.             BuyProductID(kProductIDMonth);
    98.         }
    99.         public void BuyYear()
    100.             {
    101.                 // Buy the subscription product using its the general identifier. Expect a response either
    102.                 // through ProcessPurchase or OnPurchaseFailed asynchronously.
    103.                 // Notice how we use the general product identifier in spite of this ID being mapped to
    104.                 // custom store-specific identifiers above.
    105.                 BuyProductID(kProductIDYear);
    106.             }
    107.         void BuyProductID(string productId)
    108.             {
    109.                 // If Purchasing has been initialized ...
    110.                 if (IsInitialized())
    111.                 {
    112.                     // ... look up the Product reference with the general product identifier and the Purchasing
    113.                     // system's products collection.
    114.                     Product product = m_StoreController.products.WithID(productId);
    115.                     // If the look up found a product for this device's store and that product is ready to be sold ...
    116.                     if (product != null && product.availableToPurchase)
    117.                     {
    118.                         Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));
    119.                         // ... buy the product. Expect a response either through ProcessPurchase or OnPurchaseFailed
    120.                         // asynchronously.
    121.                         m_StoreController.InitiatePurchase(product);
    122.                     }
    123.                     // Otherwise ...
    124.                     else
    125.                     {
    126.                         // ... report the product look-up failure situation
    127.                         Debug.Log("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
    128.                     }
    129.                 }
    130.                 // Otherwise ...
    131.                 else
    132.                 {
    133.                     // ... report the fact Purchasing has not succeeded initializing yet. Consider waiting longer or
    134.                     // retrying initiailization.
    135.                     Debug.Log("BuyProductID FAIL. Not initialized.");
    136.                 }
    137.             }
    138.         // Restore purchases previously made by this customer. Some platforms automatically restore purchases, like Google.
    139.         // Apple currently requires explicit purchase restoration for IAP, conditionally displaying a password prompt.
    140.         public void RestorePurchases()
    141.         {
    142.             // If Purchasing has not yet been set up ...
    143.             if (!IsInitialized())
    144.             {
    145.                 // ... report the situation and stop restoring. Consider either waiting longer, or retrying initialization.
    146.                 Debug.Log("RestorePurchases FAIL. Not initialized.");
    147.                 return;
    148.             }
    149.             // If we are running on an Apple device ...
    150.             if (Application.platform == RuntimePlatform.IPhonePlayer ||
    151.                 Application.platform == RuntimePlatform.OSXPlayer)
    152.             {
    153.                 // ... begin restoring purchases
    154.                 Debug.Log("RestorePurchases started ...");
    155.                 // Fetch the Apple store-specific subsystem.
    156.                 var apple = m_StoreExtensionProvider.GetExtension<IAppleExtensions>();
    157.                 // Begin the asynchronous process of restoring purchases. Expect a confirmation response in
    158.                 // the Action<bool> below, and ProcessPurchase if there are previously purchased products to restore.
    159.                 apple.RestoreTransactions((result) => {
    160.                     // The first phase of restoration. If no more responses are received on ProcessPurchase then
    161.                     // no purchases are available to be restored.
    162.                     Debug.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
    163.                 });
    164.             }
    165.             // Otherwise ...
    166.             else
    167.             {
    168.                 // We are not running on an Apple device. No work is necessary to restore purchases.
    169.                 Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
    170.             }
    171.         }
    172.         public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    173.         {
    174.             // Purchasing has succeeded initializing. Collect our Purchasing references.
    175.             Debug.Log("OnInitialized: PASS");
    176.            
    177.             // Overall Purchasing system, configured with products for this application.
    178.             m_StoreController = controller;
    179.             // Store specific subsystem, for accessing device-specific store features.
    180.             m_StoreExtensionProvider = extensions;
    181.         //This can be called anytime after initialization
    182.         //And it should probably be limited to Google Play and not just Android
    183. #if UNITY_ANDROID
    184.         foreach (Product p in controller.products.all)
    185.         {
    186.             if (p.hasReceipt)
    187.             {
    188.                 Data.isSubscriber = true;
    189.                 Badge.init();
    190.                 db_connect.ClosePaywall();
    191.             }
    192.         }
    193. #endif
    194. #if UNITY_IOS
    195.         // Apple will only update your receipt when it is changed, until then you will have
    196.         // to save the last one provided locally ot be able to track the subscription yourself
    197.         string localsave = PlayerPrefs.GetString ("Receipt", null);
    198.         // I store the data locally Obfuscated to make it harder to cheat
    199.         // so I must call the validator to made the Receipt readable.
    200.         // remember you must run the Obfuscater in the Unity IAP window to create the Tangle files
    201.         if(!String.IsNullOrEmpty(localsave))
    202.         {
    203.             var validator = new CrossPlatformValidator(GooglePlayTangle.Data(),
    204.                 AppleTangle.Data(), Application.identifier);
    205.             var localResult = validator.Validate (localsave);
    206.             Debug.Log ("Local Receipt: " + localResult);
    207.             foreach (IPurchaseReceipt productReceipt in localResult) {
    208.                 Debug.Log ("IsInitialized local data");
    209.                 Debug.Log(productReceipt.productID);
    210.                 Debug.Log(productReceipt.purchaseDate);
    211.                 Debug.Log(productReceipt.transactionID);
    212.                 AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
    213.                 if (null != apple) {
    214.                     Debug.Log ("On Initialized Apple:");
    215.                     Debug.Log("TransactionID: "+apple.originalTransactionIdentifier);
    216.                     Debug.Log("ExpirationDate: "+apple.subscriptionExpirationDate);
    217.                     Debug.Log("Purchase Date: "+apple.purchaseDate);
    218.                     // runs the experation time compared to the current time
    219.                     if (IsSubActive (apple)) {
    220.                         Debug.Log ("Sub is Active");
    221.                         Data.isSubscriber = true;
    222.                         Badge.init();
    223.                         db_connect.ClosePaywall();
    224.                     } else {
    225.                         Debug.Log ("Sub is NOT Active");
    226.                     }
    227.                 }
    228.             }  
    229.         }
    230. #endif
    231.     }
    232.     public void OnInitializeFailed(InitializationFailureReason error)
    233.         {
    234.             // Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
    235.             Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
    236.         }
    237.         public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
    238.         {
    239.            
    240.             // Or ... a subscription product has been purchased by this user.
    241.            if (String.Equals(args.purchasedProduct.definition.id, kProductIDMonth, StringComparison.Ordinal))
    242.             {
    243.             Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
    244.             PlayerPrefs.SetString("Receipt", args.purchasedProduct.receipt);
    245.             PlayerPrefs.Save();
    246.             // Prepare the validator with the secrets we prepared in the Editor
    247.             // obfuscation window.
    248.             var validator = new CrossPlatformValidator(GooglePlayTangle.Data(),
    249.                 AppleTangle.Data(), Application.identifier);
    250.             var result = validator.Validate(args.purchasedProduct.receipt);
    251.             // For informational purposes, we list the receipt(s)
    252.             Debug.Log("Receipt is valid.");
    253.             foreach (IPurchaseReceipt productReceipt in result) {
    254.                 Debug.Log(productReceipt.productID);
    255.                 Debug.Log(productReceipt.purchaseDate);
    256.                 Debug.Log(productReceipt.transactionID);
    257.                 GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
    258.                 if (null != google) {
    259.                     Debug.Log ("Google");
    260.                     Debug.Log(google.ToString());
    261.                     Debug.Log(google.purchaseToken);
    262.                 }
    263.                 AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
    264.                 if (null != apple) {
    265.                     Debug.Log ("ProcessPurchase Apple:");
    266.                     Debug.Log(apple.originalTransactionIdentifier);
    267.                     Debug.Log(apple.subscriptionExpirationDate);
    268.                     if (IsSubActive (apple)) {
    269.                         Debug.Log ("Sub is Active");
    270.                         Data.isSubscriber = true;
    271.                         db_connect.ClosePaywall();
    272.                         Badge.init();
    273.                     } else {
    274.                         Debug.Log ("Sub is NOT Active");
    275.                     }
    276.                 }
    277.             }
    278.         }
    279.             // Or ... a Year subscription product has been purchased by this user.
    280.             else if (String.Equals(args.purchasedProduct.definition.id, kProductIDYear, StringComparison.Ordinal))
    281.             {
    282.             Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
    283.             PlayerPrefs.SetString("Receipt", args.purchasedProduct.receipt);
    284.             PlayerPrefs.Save();
    285.             // Prepare the validator with the secrets we prepared in the Editor
    286.             // obfuscation window.
    287.             var validator = new CrossPlatformValidator(GooglePlayTangle.Data(),
    288.                 AppleTangle.Data(), Application.identifier);
    289.             var result = validator.Validate(args.purchasedProduct.receipt);
    290.             // For informational purposes, we list the receipt(s)
    291.             Debug.Log("Receipt is valid.");
    292.             foreach (IPurchaseReceipt productReceipt in result) {
    293.                 Debug.Log(productReceipt.productID);
    294.                 Debug.Log(productReceipt.purchaseDate);
    295.                 Debug.Log(productReceipt.transactionID);
    296.                 GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
    297.                 if (null != google) {
    298.                     Debug.Log ("Google");
    299.                     Debug.Log(google.ToString());
    300.                     Debug.Log(google.purchaseToken);
    301.                 }
    302.                 AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
    303.                 if (null != apple) {
    304.                     Debug.Log ("ProcessPurchase Apple:");
    305.                     Debug.Log(apple.originalTransactionIdentifier);
    306.                     Debug.Log(apple.subscriptionExpirationDate);
    307.                     if (IsSubActive (apple)) {
    308.                         Debug.Log ("Sub is Active");
    309.                         Data.isSubscriber = true;
    310.                         db_connect.ClosePaywall();
    311.                         Badge.init();
    312.                     } else {
    313.                         Debug.Log ("Sub is NOT Active");
    314.                     }
    315.                 }
    316.             }
    317.         }
    318.             // Return a flag indicating whether this product has completely been received, or if the application needs
    319.             // to be reminded of this purchase at next app launch. Use PurchaseProcessingResult.Pending when still
    320.             // saving purchased products to the cloud, and when that save is delayed.
    321.             return PurchaseProcessingResult.Complete;
    322.         }
    323.         public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    324.         {
    325.             // A product purchase attempt did not succeed. Check failureReason for more detail. Consider sharing
    326.             // this reason with the user to guide their troubleshooting actions.
    327.             Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
    328.         }
    329.     }
    330.  
    its definitely not the cleanest approach, but its the only full example on the internet at the moment. if anyone has any suggestions or questions let me know
     
  28. Haze74

    Haze74

    Joined:
    Jul 28, 2015
    Posts:
    11
    As developers we're left to figure out a great deal of things by time consuming trial and error with only contradictory and inadequate information available on internet. My Android app uses subscriptions so I can report specifically on my frustration with that.

    One obvious question you'd like to know the answer to is this: how do I check if the subscription is still active? There is no reason for why answers to simple questions such as that one should not be included on the Unity documentation pages. An answer like "this should be transparent" is not enough. Since we are not given the definitions of all the variables and methods, none of this is transparent.

    Here is a simple example of how a clear answer should look like:

    QUESTION: HOW DO I CHECK IF A SUBSCRIPTION IS ACTIVE?

    ANSWER:
    ----------------
    If the device has access to internet (see here [link provided] for how you can determine if the device has internet connection) here is what you need to do:

    1) Run UnityPurchasing.Initialize(this, builder) (see here [link provided] for code how you create the arguments)
    2) This will call the method OnInitialized which you must implement on your side (see here for a template to get you started)
    3) OnInitialized provides the variable IStoreController controller.
    4) Get your product info like this: Product p = controller.products.WithID ("your-product-name");
    5) Finally p contains a boolean p.hasReceipt. This boolean is true if the subscription is active and false if the subscription has expired.
    ------------------

    Providing definitions, descriptions, and instructions such as this would be a huge benefit for developers. That cannot take much time to write up. Our app has been delayed significantly because of lack of clear information about the Unity IAP.
     
    inovixion110 and musicdeveloper like this.
  29. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    @Haze74 Thank you for the feedback. I believe your answer is missing the links. Also, this description appears applicable to iOS only. My "transparent" comment is with regard to Google Play. If the product shows up, it's active. On iOS, you check the receipt. Our documentation team has a backlog that we are trying to work through, and improved documentation is high on our priority list, as well as improved subscription support.
     
    Sir-Gatlin likes this.
  30. nicloay

    nicloay

    Joined:
    Jul 11, 2012
    Posts:
    540
    @Sir-Gatlin
    It looks like you have copypaste at ProcessPurchase method (you can combine both subscriptions isn't it?)
    and also. at ProcessPurchase I don't see where you disable paywall for android.
     
  31. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    @nicloay Can you elaborate on disabling paywall? Is that a legitimate practice? And not clear on your copypaste comment.
     
  32. nicloay

    nicloay

    Joined:
    Jul 11, 2012
    Posts:
    540
    No it was only about codesnippet above. It has nice approach about caching receipt and check it on next start, but this code has small issues.
     
  33. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    What about your paywall comment please?
     
  34. nicloay

    nicloay

    Joined:
    Jul 11, 2012
    Posts:
    540
    I don't think that mine is better.
    but in the @Sir-Gatlin code will be better to extract
    Code (csharp):
    1.  
    2. Data.isSubscriber = true;
    3.                 Badge.init();
    4.                 db_connect.ClosePaywall();
    5.  
    to separated method
    on line 257 probably need to call this peace of code as well (because paywall I don't see where the action to remove paywall as on ios)
    Code (csharp):
    1.  
    2. if (null != google) {
    3.                    //Call extracted method to remove paywall here
    4.                     Debug.Log ("Google");
    5.                     Debug.Log(google.ToString());
    6.                     Debug.Log(google.purchaseToken);
    7.                 }
    8.  
    and combine conditions on line 240 and and 279 as those block of code is probably similar
    Code (csharp):
    1.  
    2.  if (String.Equals(args.purchasedProduct.definition.id, kProductIDMonth, StringComparison.Ordinal)){
    3. ...
    4. } else if (String.Equals(args.purchasedProduct.definition.id, kProductIDYear, StringComparison.Ordinal)){
    5. ... //the same receipt handlers
    6. }
    7.  
    It's just suggestions.
     
  35. Sir-Gatlin

    Sir-Gatlin

    Joined:
    Jan 18, 2013
    Posts:
    28
    So I am now working on a one time pop up to congratulate users when they subscribe. I assume I would put the pop up under ProcessPurchase()? but will that make it pop up every time they log on and the system verifies their subscription? @JeffDUnity3D can you confirm if this is the best method to cover a one time pop up? or am I missing something?
     
  36. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    @Sir-Gatlin I would recommend that you test your approach, and see how it works for you. You might need some client-based inventory management system, like storing in PlayerPrefs. But be sure to test the uninstall/reinstall scenario.
     
    Sir-Gatlin likes this.
  37. andymads

    andymads

    Joined:
    Jun 16, 2011
    Posts:
    1,614
    I don't believe this is correct. I've been testing on the iOS Sandbox and hasReceipt can be true yet according to the app receipt the subscription has expired, i.e the most recent expiry time has passed.
     
  38. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    Yes, this doesn't apply on iOS, only Google Play. You want to check the receipt on iOS.
     
  39. kevinatgame

    kevinatgame

    Joined:
    Aug 31, 2015
    Posts:
    8
    hey @JeffDUnity3D , as you mentioned, Unity team is working on improving the support of subscription and it's document. but is there a ETA for it?
    and, unfortunately, I keep receiving product receipt when OnInitialized, and the product.hasreceipt is true as well after I cancelled the subscriptions from google play console couple days ago.
    I'm using a test account to pay and test my game on android phone. but I guess the test account thing is transparent for unity iap.
     
    Last edited: Feb 9, 2018
  40. andymads

    andymads

    Joined:
    Jun 16, 2011
    Posts:
    1,614
    Well why did you reply to that post with this then?

    " Also, this description appears applicable to iOS only"
     
  41. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    Please read my next statements after that - "My transparent comment is with regard to Google Play. If the product shows up, it's active. On iOS, you check the receipt"
     
  42. rattlesnake

    rattlesnake

    Joined:
    Jul 18, 2013
    Posts:
    138
    Yes please, we would like to know ! We are in 2018 and it's still difficult to track a such basic thing :/
     
  43. andymads

    andymads

    Joined:
    Jun 16, 2011
    Posts:
    1,614
    Dropped from where?
     
  44. andymads

    andymads

    Joined:
    Jun 16, 2011
    Posts:
    1,614
    Shows up where?
     
  45. rattlesnake

    rattlesnake

    Joined:
    Jul 18, 2013
    Posts:
    138
    Hello guys,
    Every thing is ok for me with Android, but not with Apple.

    I try to test if a user cancel an Auto-renewable subscription on Apple. It seems a real pain as we have wait some cycles to end and we cannot just unsubscribe (thank you Apple...).
    Do you know a better process to cancel a sandbox subscription?

    Sir-Gatlin I use your code with the "Receipt" saved in the PlayerPrefs. And it seems to work great however, any one can confirm that if the user then unsubscribe, it's not a problem to store the receipt locally ?

    And if the user delete/install the app, I just have to call the RestorePurchases() right ?
     
  46. hm_assets

    hm_assets

    Joined:
    Sep 30, 2016
    Posts:
    6
    Why do you save receipt locally in the PlayerPrefs, while you can get it every time after OnInitialised from IAppleConfiguration:

    If apple subscription was renewed, the expiration date will be new. You can just check timeToExpiry from the code below

    My example:
    Code (CSharp):
    1.  
    2. var appleConfig = _builder.Configure<IAppleConfiguration>();
    3.             if (!string.IsNullOrEmpty(appleConfig.appReceipt))
    4.             {
    5.                 var receiptData = System.Convert.FromBase64String(appleConfig.appReceipt);
    6.                 AppleReceipt receipt = new AppleValidator (AppleTangle.Data()).Validate(receiptData);
    7.                 if (receipt != null)
    8.                 {
    9.                     var productReceipts = receipt.inAppPurchaseReceipts;
    10.                     for (int i = productReceipts.Length-1; i >= 0; i--)
    11.                     {
    12.                         var productReceipt = productReceipts[i];
    13.                    
    14.                         if (productReceipt.productID.Contains(kSubscrIAPProductIDPrefix))
    15.                         {
    16.                             var utcOffset = DateTime.Now - DateTime.UtcNow;
    17.                             var newDateToCheck = productReceipt.subscriptionExpirationDate + utcOffset;
    18.                             var timeToExpiry = productReceipt.subscriptionExpirationDate - DateTime.UtcNow;
    19.  
    20.                             Debug.Log("InAppManager: Time to expiry = " + timeToExpiry);
    21.                             if (timeToExpiry.TotalSeconds > 0)
    22.                             {
    23.                                 MyDebug.Log("InAppManager: PRODUCTID: " + productReceipt.productID);
    24.                                 MyDebug.Log("InAppManager: PURCHASE DATE: " + productReceipt.purchaseDate);
    25.                                 MyDebug.Log("InAppManager: EXPIRATION DATE: " + productReceipt.subscriptionExpirationDate);
    26.                                 validReceiptFound = true;
    27.                             }
    28.                             break;
    29.                         }
    30.                     }
    31.                 }              
    32.             }
    33.  
     
  47. andymads

    andymads

    Joined:
    Jun 16, 2011
    Posts:
    1,614
    This is pretty much what I do.
     
  48. rattlesnake

    rattlesnake

    Joined:
    Jul 18, 2013
    Posts:
    138
    Thank you i will try that great exemple :)
    BTW no news about Unity "improved subscription support" ?
     
  49. SelmanBMG

    SelmanBMG

    Joined:
    Feb 20, 2018
    Posts:
    1
    Hi @Sir-Gatlin,

    Thank you so much for posting this - you've already saved me a lot of time and headaches. I was feeling pretty directionless for awhile there...

    Would you mind expanding on this process? I've found the Receipt Validation Obfuscator, but I'm not sure of the next step. Data.isSubscriber gives me the " 'Data' does not exist in this context" error. I'm uncertain as to what kind of "key" to enter (I'm not using the Google Play store, just iOS) or what the next step should be to store data locally as you did.

    Any help is greatly appreciated. Thanks again!
     
  50. anuragclancy

    anuragclancy

    Joined:
    Jul 5, 2016
    Posts:
    4
    Facing the same issue. product.hasReceipt is true even after cancelling the subscription using test account. Can someone shed some light on this. Is it because I am using a test account? autoRenew returns false after I cancel the subscription but I can't use that as Google requires to allow access to the content for the amount of time the user signed up for even after cancelling the subscription.
     
    Last edited: Mar 23, 2018
Thread Status:
Not open for further replies.