// // ©2015 - 2025 Candy Smith // // All rights reserved // // Redistribution of this software is strictly not allowed. // // Copy of this software can be obtained from unity asset store only. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE // // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // // THE SOFTWARE. using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Purchasing; using UnityEngine.Purchasing.Extension; namespace WordsToolkit.Scripts.Services.IAP { #if UNITY_PURCHASING public class IAPController : IDetailedStoreListener, IIAPService { private static IStoreController storeController; private IExtensionProvider extensionProvider; public static event Action OnSuccessfulPurchase; public static event Action<(string,string)> OnFailedPurchase; public static event Action> OnRestorePurchasesFinished; public void InitializePurchasing(IEnumerable<(string productId, ProductTypeWrapper.ProductType productType)> products) { if (IsInitialized()) { return; } var standardPurchasingModule = StandardPurchasingModule.Instance(); // #if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WEBGL // standardPurchasingModule.useFakeStoreUIMode = FakeStoreUIMode.StandardUser; // standardPurchasingModule.useFakeStoreAlways = true; // #endif var builder = ConfigurationBuilder.Instance(standardPurchasingModule); foreach (var (productId, productType) in products) { builder.AddProduct(productId, ProductTypeWrapper.GetProductType(productType)); } UnityPurchasing.Initialize(this, builder); } public bool IsInitialized() { return storeController != null; } public void OnInitialized(IStoreController controller, IExtensionProvider extensions) { extensionProvider = extensions; storeController = controller; RestorePurchases((success, restoredProducts) => { if (success) { Debug.Log($"Restore purchases succeeded. Restored products: {string.Join(", ", restoredProducts)}"); foreach (var productId in restoredProducts) { MarkProductAsPurchased(productId); } } }); } public void Restore(Action> action) { RestorePurchases((success, restoredProducts) => { if (success) { action?.Invoke(true, restoredProducts); Debug.Log($"Restore purchases succeeded. Restored products: {string.Join(", ", restoredProducts)}"); foreach (var productId in restoredProducts) { MarkProductAsPurchased(productId); } } }); } private void MarkProductAsPurchased(string productId) { PlayerPrefs.SetInt("Purchased_" + productId, 1); PlayerPrefs.Save(); } public bool IsProductPurchased(string productId) { return PlayerPrefs.GetInt("Purchased_" + productId, 0) == 1; } public void BuyProduct(string productId) { try { if (IsInitialized()) { var product = storeController.products.WithID(productId); if (product != null && product.availableToPurchase) { Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id)); storeController.InitiatePurchase(product); OnFailedPurchase?.Invoke((productId, "Product not found or not available for purchase.")); // debug only } else { Debug.Log($"BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase {productId}"); OnFailedPurchase?.Invoke((productId, "InvalidProductID: product not found or not available for purchase.")); } } else { Debug.Log("IAPInitFailed: BuyProductID FAIL. Not initialized."); OnFailedPurchase?.Invoke((productId, "IAPInitFailed: Not initialized.")); } } catch (Exception e) { Debug.Log("BuyProductID: FAIL. Exception during purchase. " + e); OnFailedPurchase?.Invoke((productId, "Exception during purchase: " + e.Message)); } } public decimal GetProductLocalizedPrice(string productId) { if (IsInitialized()) { var product = storeController.products.WithID(productId); if (product != null) { return product.metadata.localizedPrice; } } return 0m; } public string GetProductLocalizedPriceString(string productId) { if (IsInitialized()) { var product = storeController.products.WithID(productId); if (product != null) { return product.metadata.localizedPriceString; } } return string.Empty; } public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription) { Debug.Log("OnPurchaseFailed: FAIL. Product: " + product.definition.id + " PurchaseFailureDescription: " + failureDescription); OnFailedPurchase?.Invoke((product.definition.id, failureDescription.message)); } public void OnPurchaseFailed(Product i, PurchaseFailureReason p) { Debug.Log($"OnPurchaseFailed: FAIL. Product: '{i.definition.id}', PurchaseFailureReason: {p}"); OnFailedPurchase?.Invoke((i.definition.id, p.ToString())); } public void OnInitializeFailed(InitializationFailureReason reason) { Debug.Log("OnInitializeFailed InitializationFailureReason:" + reason); } public void OnInitializeFailed(InitializationFailureReason error, string message) { Debug.Log("OnInitializeFailed InitializationFailureReason:" + error + " message: " + message); } public void RestorePurchases(Action> onRestore) { if (!IsInitialized()) { Debug.Log("RestorePurchases FAIL. Not initialized."); onRestore(false, new List()); return; } var restoredProducts = new List(); Action restoreCallback = success => { if (success) { foreach (var product in storeController.products.all) { if (product.hasReceipt) { restoredProducts.Add(product.definition.id); } } } Debug.Log($"RestorePurchases finished. Success: {success}, Restored products: {string.Join(", ", restoredProducts)}"); onRestore(success, restoredProducts); OnRestorePurchasesFinished?.Invoke(success, restoredProducts); }; if (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.OSXPlayer) { Debug.Log("RestorePurchases started ..."); var apple = extensionProvider.GetExtension(); apple.RestoreTransactions((result, message) => { Debug.Log("RestorePurchases continuing: " + message); restoreCallback(result); }); } else if (Application.platform == RuntimePlatform.Android) { var googlePlayStoreExtensions = extensionProvider.GetExtension(); googlePlayStoreExtensions.RestoreTransactions((success, message) => { if (success) { Debug.Log("Transactions restored successfully: " + message); } else { Debug.LogError("Failed to restore transactions: " + message); } restoreCallback(success); }); } else { Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform); restoreCallback(false); } } public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args) { Debug.Log($"ProcessPurchase: PASS. Product: '{args.purchasedProduct.definition.id}'"); if (args.purchasedProduct.definition.type == ProductType.NonConsumable) { MarkProductAsPurchased(args.purchasedProduct.definition.id); } OnSuccessfulPurchase?.Invoke(args.purchasedProduct.definition.id); return PurchaseProcessingResult.Complete; } } #endif }