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. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Resolved Scripted IAP Question

Discussion in 'Unity IAP' started by Munchy2007, Mar 17, 2022.

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

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,732
    @JeffDUnity3D

    Following on from posts in this thread https://forum.unity.com/threads/uni...t-initialized-correctly.1252680/#post-7965567

    I'm working on converting from Codeless IAP to Scripted IAP, but it appears that for Unity Purchasing to function correctly, the Product Catalog still needs to be set up in the editor (Window->Unity IAP->IAP Catalog...), rather than me setting it up entirely in my own script.

    I've unticked Automatically Initialize UnityPurchasing in the IAP Catalog and I'm doing this in my IAPManager script to initialize purchasing using the catalog items.
    Code (CSharp):
    1. public void InitializePurchasing()
    2. {
    3.     Debug.Log("InitializePurchasing");
    4.     if (IsInitialized())
    5.     {
    6.         return;
    7.     }
    8.  
    9.     StandardPurchasingModule module = StandardPurchasingModule.Instance();
    10.     module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
    11.  
    12.     var builder = ConfigurationBuilder.Instance(module);
    13.  
    14.     var catalog = ProductCatalog.LoadDefaultCatalog();
    15.     foreach (ProductCatalogItem product in catalog.allValidProducts)
    16.     {
    17.         IDs ids = null;
    18.  
    19.         if (product.allStoreIDs.Count > 0)
    20.         {
    21.             ids = new IDs();
    22.             foreach (var storeID in product.allStoreIDs)
    23.             {
    24.                 ids.Add(storeID.id, storeID.store);
    25.             }
    26.         }
    27.  
    28.         var payoutDefinitions = new List<PayoutDefinition>();
    29.         foreach (var payout in product.Payouts)
    30.         {
    31.             payoutDefinitions.Add(new PayoutDefinition( payout.typeString,
    32.                                                         payout.subtype,
    33.                                                         payout.quantity,
    34.                                                         payout.data));
    35.         }
    36.         builder.AddProduct(product.id, product.type, ids, payoutDefinitions.ToArray());
    37.     }
    38.     UnityPurchasing.Initialize(this, builder);
    39. }
    Before I go further with converting all my apps, I just wanted to ask if this is the correct approach to take so that I can avoid the problems you described that can affect Codeless IAP.

    Thanks.
     
    Last edited: Mar 17, 2022
    Rachan likes this.
  2. Munchy2007

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,732
    So in the absence of any information to the contrary, or real world examples that work without using the editor to build the catalog, I'm going to assume this is the recommended way to go.

    It's all working as expected so far anyway :)
     
  3. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    You would have better luck posting in the IAP forum. You can do it with the catalog, but I prefer more control over the process and not use the Catalog at all. Please refer to the Sample IAP Project v3 as an example https://forum.unity.com/threads/sample-iap-project.529555/#post-7922275
     
  4. Munchy2007

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,732
    Thanks for getting back to me, you can move this to the IAP forum if you think it would be better there.

    I agree with you that having more control is better, and initially, based on what you had said, I was actually expecting to provide all the information via scripting, but when I followed the method in the V3 sample you linked (and others), I was finding I was missing information in the PurchaseProcessingResult args that I would have had if I used the Catalog, which at the time I thought was going to be an issue.

    However, I will go and take a look at it again, as I'm more familiar with the whole thing now and I may spot something I missed previously. If I get stuck I'll come back with further questions if that's okay?
     
  5. Munchy2007

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,732
    I've come to the realization that the majority of the information entered into the Product catalog in the editor is for use only when running the game in the editor and isn't therefore strictly necessary, this wasn't clear initially.

    Anyway, having figured that out, it is much easier to build and maintain a large list of in app products by script than it was with the editor catalog, which gets increasingly awkward to use as the product list grows.

    So I've got Scripted API up and running now, with no reliance on the editor catalog, or any other codeless components and it's much more streamlined.

    Thanks for keep nudging me in the right direction, I got there in the end :)
     
    BluSquare_Games likes this.
  6. BluSquare_Games

    BluSquare_Games

    Joined:
    Apr 17, 2022
    Posts:
    4


    Hi Munchy2007, can you provide an example of how you implemented scripted IAP? I am new to this and any help would be appreciated. Thanks!
     
  7. Munchy2007

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,732
    Hi @blucat_studios this is my basic implementation,

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using UnityEngine;
    5. using UnityEngine.Purchasing;
    6. using UnityEngine.Purchasing.Security;
    7.  
    8. namespace Doofah.IAP
    9. {
    10.     public class IAPManager : MonoBehaviour, IStoreListener
    11.     {
    12.         public const string Remove_Ads_ID = "remove_ads";
    13.         private static IStoreController m_StoreController;          // The Unity Purchasing system.
    14.         private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
    15.         static CrossPlatformValidator m_Validator = null;
    16.  
    17.         public static event System.Action onIAPManagerInitialised;
    18.         public static event System.Action<PayoutDefinition> onCurrencyPurchased;
    19.         public static event System.Action<Product> onNonConsumablePurchased;
    20.  
    21.         public static bool IAPButtonHasBeenUsed { get; set; }
    22.  
    23.         public static bool IsInitialized()
    24.         {
    25.             return m_StoreController != null && m_StoreExtensionProvider != null;
    26.         }
    27.  
    28.         private void Awake()
    29.         {
    30.             IAPButtonHasBeenUsed = false;
    31.         }
    32.  
    33.         private void Start()
    34.         {
    35.             //Sound.SoundManager.StopMusic();
    36.             InitializePurchasing();
    37.         }
    38.  
    39.         public void InitializePurchasing()
    40.         {
    41.             Debug.Log("InitializePurchasing");
    42.             if (IsInitialized())
    43.             {
    44.                 return;
    45.             }
    46.  
    47.             StandardPurchasingModule module = StandardPurchasingModule.Instance();
    48.             module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
    49.  
    50.             var builder = ConfigurationBuilder.Instance(module);
    51.  
    52.             foreach (var product in IAPCatalog.products)
    53.             {
    54.                 builder.AddProduct(product.name, product.type, product.storeIDs, product.payouts.ToArray());
    55.             }
    56.  
    57.             UnityPurchasing.Initialize(this, builder);
    58.         }
    59.  
    60.         void InitializeValidator()
    61.         {
    62.             if (IsCurrentStoreSupportedByValidator())
    63.             {
    64. #if !UNITY_EDITOR
    65.                 m_Validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
    66. #endif
    67.             }
    68.             else
    69.             {
    70.                 Debug.LogWarning($"The cross-platform validator is not implemented for the currently selected store: {StandardPurchasingModule.Instance().appStore}.\nBuild the project for Android, iOS, macOS, or tvOS and use the Google Play Store or Apple App Store. See README for more information.");
    71.             }
    72.         }
    73.  
    74.         public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    75.         {
    76.             Debug.Log($"OnInitialized {controller} {extensions}");
    77.             m_StoreController = controller;
    78.             m_StoreExtensionProvider = extensions;
    79.             InitializeValidator();
    80.             ListProducts();
    81.             onIAPManagerInitialised?.Invoke();
    82.         }
    83.  
    84.         public void OnInitializeFailed(InitializationFailureReason error)
    85.         {
    86.             Debug.Log($"OnInitializeFailed {error}");
    87.         }
    88.  
    89.         static bool IsCurrentStoreSupportedByValidator()
    90.         {
    91.             //The CrossPlatform validator only supports the GooglePlayStore and Apple's App Stores.
    92.             return IsGooglePlayStoreSelected() || IsAppleAppStoreSelected();
    93.         }
    94.  
    95.         static bool IsGooglePlayStoreSelected()
    96.         {
    97.             var currentAppStore = StandardPurchasingModule.Instance().appStore;
    98.             return currentAppStore == AppStore.GooglePlay;
    99.         }
    100.  
    101.         static bool IsAppleAppStoreSelected()
    102.         {
    103.             var currentAppStore = StandardPurchasingModule.Instance().appStore;
    104.             return currentAppStore == AppStore.AppleAppStore ||
    105.                    currentAppStore == AppStore.MacAppStore;
    106.         }
    107.  
    108.         public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
    109.         {
    110.             //Retrieve the purchased product
    111.             var product = args.purchasedProduct;
    112.  
    113.             var isPurchaseValid = IsPurchaseValid(product);
    114.  
    115.             if (isPurchaseValid)
    116.             {
    117.                 Debug.Log("Valid receipt, unlocking content.");
    118.                 //Add the purchased product to the players inventory
    119.                 foreach (var payout in product.definition.payouts)
    120.                 {
    121.                     if (payout.type == PayoutType.Currency)
    122.                     {
    123.                         onCurrencyPurchased?.Invoke(payout);
    124.                     }
    125.                     else if(payout.type == PayoutType.Other)
    126.                     {
    127.                         onNonConsumablePurchased?.Invoke(product);
    128.                     }
    129.                 }
    130.  
    131.                 if(IAPButtonHasBeenUsed)
    132.                 {
    133.                     //UIManager.ShowUI(ThanksForPurchaseWindow.Instance);
    134.                 }
    135.             }
    136.             else
    137.             {
    138.                 if(IAPButtonHasBeenUsed)
    139.                 {
    140.                     //UIManager.ShowUI(PurchaseFailedWindow.Instance);
    141.                 }
    142.                 Debug.Log("Invalid receipt, not unlocking content.");
    143.             }
    144.  
    145.             Debug.Log($"Purchase Complete - Product: {product.definition.id}");
    146.  
    147.             //We return Complete, informing IAP that the processing on our side is done and the transaction can be closed.
    148.             return PurchaseProcessingResult.Complete;
    149.         }
    150.  
    151.         static bool IsPurchaseValid(Product product)
    152.         {
    153.             //If we the validator doesn't support the current store, we assume the purchase is valid
    154.             if (IsCurrentStoreSupportedByValidator())
    155.             {
    156.                 try
    157.                 {
    158.                     var result = m_Validator.Validate(product.receipt);
    159.                     //The validator returns parsed receipts.
    160.                     LogReceipts(result);
    161.                 }
    162.                 //If the purchase is deemed invalid, the validator throws an IAPSecurityException.
    163.                 catch (IAPSecurityException reason)
    164.                 {
    165.                     Debug.Log($"Invalid receipt: {reason}");
    166.                     return false;
    167.                 }
    168.             }
    169.  
    170.             return true;
    171.         }
    172.  
    173.         static void LogReceipts(IEnumerable<IPurchaseReceipt> receipts)
    174.         {
    175.             Debug.Log("Receipt is valid. Contents:");
    176.             foreach (var receipt in receipts)
    177.             {
    178.                 LogReceipt(receipt);
    179.             }
    180.         }
    181.  
    182.         static void LogReceipt(IPurchaseReceipt receipt)
    183.         {
    184.             Debug.Log($"Product ID: {receipt.productID}\n" +
    185.                       $"Purchase Date: {receipt.purchaseDate}\n" +
    186.                       $"Transaction ID: {receipt.transactionID}");
    187.  
    188.             if (receipt is GooglePlayReceipt googleReceipt)
    189.             {
    190.                 Debug.Log($"Purchase State: {googleReceipt.purchaseState}\n" +
    191.                           $"Purchase Token: {googleReceipt.purchaseToken}");
    192.             }
    193.  
    194.             if (receipt is AppleInAppPurchaseReceipt appleReceipt)
    195.             {
    196.                 Debug.Log($"Original Transaction ID: {appleReceipt.originalTransactionIdentifier}\n" +
    197.                           $"Subscription Expiration Date: {appleReceipt.subscriptionExpirationDate}\n" +
    198.                           $"Cancellation Date: {appleReceipt.cancellationDate}\n" +
    199.                           $"Quantity: {appleReceipt.quantity}");
    200.             }
    201.         }
    202.  
    203.         public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    204.         {
    205.             UIManager.ShowUI(PurchaseFailedWindow.Instance);
    206.             Debug.Log($"Purchase failed - Product: '{product.definition.id}', PurchaseFailureReason: {failureReason}");
    207.         }
    208.  
    209.         public static void BuyItem(string productID)
    210.         {
    211.             if (string.IsNullOrEmpty(productID))
    212.             {
    213.                 Debug.LogError($"IAPManager.BuyItem - null or empty product ID");
    214.                 return;
    215.             }
    216.             m_StoreController.InitiatePurchase(productID);
    217.         }
    218.  
    219.         public static bool GetProduct(string productID, out Product product)
    220.         {
    221.             product = m_StoreController.products.WithID(productID);
    222.             return product.availableToPurchase;
    223.         }
    224.  
    225.         public static bool CanBePurchased(string productID)
    226.         {
    227.             if(string.IsNullOrEmpty(productID))
    228.             {
    229.                 Debug.LogError($"IAPManager.CanBePurchased - null or empty product ID");
    230.                 return false;
    231.             }
    232.             GetProduct(productID, out Product product);
    233.             return product != null && product.availableToPurchase;
    234.         }
    235.  
    236.         public void ListProducts()
    237.         {
    238.             //AdManager.SetAdsState(AdManager.AdsState.enabled);
    239.  
    240.             foreach (Product item in m_StoreController.products.all)
    241.             {
    242.                 if (item.receipt != null)
    243.                 {
    244.                     Debug.Log("Receipt found for Product = " + item.definition.id.ToString());
    245.                     try
    246.                     {
    247.                         var result = m_Validator.Validate(item.receipt);
    248.                         //The validator returns parsed receipts.
    249.                         Debug.Log($"Receipt is valid = {result}");
    250.                         if(item.definition.id == Remove_Ads_ID)
    251.                         {
    252.                             //AppAchievementManager.UnlockAdFreeAchievement();
    253.                             //AdManager.SetAdsState(AdManager.AdsState.disabled);
    254.                         }
    255.                     }
    256.                     //If the purchase is deemed invalid, the validator throws an IAPSecurityException.
    257.                     catch (IAPSecurityException reason)
    258.                     {
    259.                         Debug.Log($"Invalid receipt: {reason}");
    260.                         if (item.definition.id == Remove_Ads_ID)
    261.                         {
    262.                             IAPDatabase.RemoveProduct(item.definition.id);
    263.                             //AdManager.SetAdsState(AdManager.AdsState.enabled);
    264.                         }
    265.                     }
    266.                 }
    267.             }
    268.  
    269.             // This is to set the ads state when palying in the Editor
    270.             if(!IsCurrentStoreSupportedByValidator())
    271.             {
    272.                 //AdManager.SetAdsState(IAPDatabase.Contains(Remove_Ads_ID) ? AdManager.AdsState.disabled: AdManager.AdsState.enabled);
    273.             }
    274.         }
    275.  
    276.         public static bool IsPurchased(string productID)
    277.         {
    278.             if (string.IsNullOrEmpty(productID))
    279.             {
    280.                 Debug.LogError($"IAPManager.IsPurchased - null or empty product ID");
    281.                 return false;
    282.             }
    283.             GetProduct(productID, out Product product);
    284.             return product != null && product.hasReceipt && IsPurchaseValid(product);
    285.         }
    286.     }
    287. }
    288.  
    289.  
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. namespace Doofah.IAP
    6. {
    7.     public static class IAPCatalog
    8.     {
    9.         public static IAPCatalogProduct[] products;
    10.         static Dictionary<string, IAPCatalogProduct> productsDict;
    11.        
    12.         [RuntimeInitializeOnLoadMethod]
    13.         public static void LoadProducts()
    14.         {
    15.             products = Resources.LoadAll<IAPCatalogProduct>("IAPProducts");
    16.             productsDict = new Dictionary<string, IAPCatalogProduct>();
    17.             foreach(var product in products)
    18.             {
    19.                 productsDict.Add(product.name, product);
    20.             }
    21.         }
    22.  
    23.         public static IAPCatalogProduct Get(string productID)
    24.         {
    25.             productsDict.TryGetValue(productID, out IAPCatalogProduct product);
    26.             return product;
    27.         }
    28.  
    29.         public static string GetDescription(string productID)
    30.         {
    31.             if(productsDict.TryGetValue(productID, out IAPCatalogProduct product))
    32.             {
    33.                 return product.Description();
    34.             }
    35.             else
    36.             {
    37.                 return "Missing Product";
    38.             }
    39.         }
    40.     }
    41. }
    42.  
    43.  
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Purchasing;
    5.  
    6. namespace Doofah.IAP
    7. {
    8.     [CreateAssetMenu]
    9.     public class IAPCatalogProduct :ScriptableObject
    10.     {
    11.         //public string id;
    12.         public ProductType type;
    13.         public List<PayoutDefinition> payouts = new List<PayoutDefinition>();
    14.         public IDs storeIDs = new IDs();
    15.         public string Description()
    16.         {
    17.             builder.Clear();
    18.             for(int n = 0; n < payouts.Count; n++)
    19.             {
    20.                 var payout = payouts[n];
    21.                 builder.Append(payout.quantity).Append(" ").Append(payout.subtype);
    22.                 if (n < payouts.Count - 1) builder.AppendLine();
    23.             }
    24.             return builder.ToString();
    25.         }
    26.  
    27.         System.Text.StringBuilder builder = new System.Text.StringBuilder(50);
    28.     }
    29. }
    30.  
    31.  
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Purchasing;
    5.  
    6. namespace Doofah.IAP
    7. {
    8.     public class IAPDatabase
    9.     {
    10.         static DBContents db;
    11.  
    12.         [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    13.         public static void Init()
    14.         {
    15.             IAPManager.onNonConsumablePurchased += IAPManager_onNonConsumablePurchased;
    16.             db = new DBContents();
    17.             db.Load();
    18.         }
    19.  
    20.         private static void IAPManager_onNonConsumablePurchased(Product product)
    21.         {
    22.             db.AddItem(product);
    23.             db.Save();
    24.         }
    25.  
    26.         public static bool Contains(string productID)
    27.         {
    28.             return db.contents.Contains(productID);
    29.         }
    30.  
    31.         public static void RemoveProduct(string productID)
    32.         {
    33.             db.contents.Remove(productID);
    34.             db.Save();
    35.         }
    36.  
    37.         [System.Serializable]
    38.         private class DBContents
    39.         {
    40.             [SerializeField] internal List<string> contents = new List<string>();
    41.  
    42.             internal void Save()
    43.             {
    44.                 PlayerPrefs.SetString("Products", JsonUtility.ToJson(this));
    45.                 PlayerPrefs.Save();
    46.             }
    47.  
    48.             internal void Load()
    49.             {
    50.                 JsonUtility.FromJsonOverwrite(PlayerPrefs.GetString("Products", "{}"), this);
    51.             }
    52.  
    53.             internal void AddItem(Product product)
    54.             {
    55.                 foreach (var item in contents)
    56.                 {
    57.                     if (item == product.definition.id)
    58.                     {
    59.                         Debug.LogWarning("Item is already in database");
    60.                         return;
    61.                     }
    62.                 }
    63.                 contents.Add(product.definition.id);
    64.             }
    65.         }
    66.     }
    67. }
    68.  
    69.  
    This implementation is incomplete in as much as it doesn't handled deferred payments and I have commented out some calls to my app specific classes.

    IAPManager should be added to a GameObject in your first scene. (You'll probably want to add a DontDestroyOnLoad call and checks to ensure only one instance is created.)

    IAPCataologProducts are ScriptableObject assets and need to be contained in a folder ...Resources/IAPProducts, for the code to work as is.

    IAPCatalog and IAPDatabase scripts just need to be somewhere in your project, but the IAPDatabase makes use of NewtonSoft's JSON Package.

    Unfortunately I haven't the time spare to be able to give detailed help on how it all works, but hopefully it'll give you an idea of where to start.
     
    BluSquare_Games likes this.
  8. BluSquare_Games

    BluSquare_Games

    Joined:
    Apr 17, 2022
    Posts:
    4
    Thank you so much @Munchy2007, this is very appreciated!
     
    Munchy2007 likes this.
  9. GiomGots

    GiomGots

    Joined:
    Apr 3, 2021
    Posts:
    13
    Thanks for the great insight on what you've done.
    I felt like i was stuck with the codeless IAP as unity is pushing it so hard I had a really, really hard time just to find the name of the "codefull IAP" => scripted IAP

    Your code sample is a great help to get rid of the oversimplified codeless IAP and I would like you to know that you're a big, big help.
    Thanks for your time
     
    Munchy2007 and Arnaud_Gorain like this.
  10. Arnaud_Gorain

    Arnaud_Gorain

    Unity Technologies

    Joined:
    Jun 28, 2022
    Posts:
    172
    This thread is now closed. Feel free to reach out via a new thread if you encounter further issue.
    Thanks!
     
Thread Status:
Not open for further replies.