Initial commit: Unity WordConnect project

This commit is contained in:
2025-08-01 19:12:05 +08:00
commit f14db75802
3503 changed files with 448337 additions and 0 deletions

View File

@ -0,0 +1,26 @@
{
"name": "CandySmith.IAP",
"rootNamespace": "",
"references": [
"GUID:60bfecf5cb232594891bc622f40d6bed",
"GUID:08d1c582746949b40ba6a45cdb776bdf",
"GUID:fe25561d224ed4743af4c60938a59d0b",
"GUID:70ea675efa2644cef98c7ece24158333",
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [
{
"name": "com.unity.purchasing",
"expression": "",
"define": "UNITY_PURCHASING"
}
],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ac6e78962cfc743b9a5fc5f5a808aa72
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,258 @@
// // ©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<string> OnSuccessfulPurchase;
public static event Action<bool, List<string>> 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<bool, List<string>> 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);
}
else
{
Debug.Log($"BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase {productId}");
}
}
else
{
Debug.Log("BuyProductID FAIL. Not initialized.");
}
}
catch (Exception e)
{
Debug.Log("BuyProductID: FAIL. Exception during purchase. " + e);
}
}
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);
}
public void OnPurchaseFailed(Product i, PurchaseFailureReason p)
{
Debug.Log($"OnPurchaseFailed: FAIL. Product: '{i.definition.id}', PurchaseFailureReason: {p}");
}
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<bool, List<string>> onRestore)
{
if (!IsInitialized())
{
Debug.Log("RestorePurchases FAIL. Not initialized.");
onRestore(false, new List<string>());
return;
}
var restoredProducts = new List<string>();
Action<bool> 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<IAppleExtensions>();
apple.RestoreTransactions((result, message) =>
{
Debug.Log("RestorePurchases continuing: " + message);
restoreCallback(result);
});
}
else if (Application.platform == RuntimePlatform.Android)
{
var googlePlayStoreExtensions = extensionProvider.GetExtension<IGooglePlayStoreExtensions>();
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
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 52d5d6d92bf64249bacbd9aca992fd28
timeCreated: 1709972098

View File

@ -0,0 +1,123 @@
// // ©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 System.Threading.Tasks;
using UnityEngine;
using VContainer;
#if UNITY_PURCHASING
#endif
namespace WordsToolkit.Scripts.Services.IAP
{
public class IAPManager : MonoBehaviour, IIAPManager
{
private IIAPService iapController;
[Inject]
public void Construct(IIAPService iapService)
{
iapController = iapService;
}
public async Task InitializePurchasing(IEnumerable<(string productId, ProductTypeWrapper.ProductType productType)> products)
{
#if UNITY_PURCHASING
if (iapController is IAPController controller)
{
controller.InitializePurchasing(products);
while (!controller.IsInitialized())
{
await Task.Delay(100);
}
}
#endif
}
public void SubscribeToPurchaseEvent(Action<string> purchaseHandler)
{
#if UNITY_PURCHASING
IAPController.OnSuccessfulPurchase += purchaseHandler;
#endif
}
public void UnsubscribeFromPurchaseEvent(Action<string> purchaseHandler)
{
#if UNITY_PURCHASING
IAPController.OnSuccessfulPurchase -= purchaseHandler;
#endif
}
public void BuyProduct(string productId)
{
iapController.BuyProduct(productId);
}
public decimal GetProductLocalizedPrice(string productId)
{
return iapController.GetProductLocalizedPrice(productId);
}
public string GetProductLocalizedPriceString(string productId)
{
return iapController.GetProductLocalizedPriceString(productId);
}
public bool IsProductPurchased(string productId)
{
#if UNITY_PURCHASING
if (iapController is IAPController controller)
{
return controller.IsProductPurchased(productId);
}
#endif
return false;
}
public void RestorePurchases(Action<bool, List<string>> action)
{
#if UNITY_PURCHASING
if (iapController is IAPController controller)
{
controller.Restore(action);
}
#endif
}
}
public class DummyIAPService : IIAPService
{
public void InitializePurchasing(IEnumerable<(string productId, ProductTypeWrapper.ProductType productType)> products)
{
}
public void BuyProduct(string productId)
{
}
public decimal GetProductLocalizedPrice(string productId)
{
return 0m;
}
public string GetProductLocalizedPriceString(string productId)
{
return string.Empty;
}
public bool IsInitialized()
{
return true;
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 4ff980a1000879c44b057af715d834ae
timeCreated: 1458726993
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace WordsToolkit.Scripts.Services.IAP
{
public interface IIAPManager
{
Task InitializePurchasing(IEnumerable<(string productId, ProductTypeWrapper.ProductType productType)> products);
void BuyProduct(string productId);
decimal GetProductLocalizedPrice(string productId);
string GetProductLocalizedPriceString(string productId);
bool IsProductPurchased(string productId);
void RestorePurchases(Action<bool, List<string>> action);
void SubscribeToPurchaseEvent(Action<string> purchaseHandler);
void UnsubscribeFromPurchaseEvent(Action<string> purchaseHandler);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b9df0059ab0544edf813589961dc11d2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,25 @@
// // ©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.Collections.Generic;
namespace WordsToolkit.Scripts.Services.IAP
{
public interface IIAPService
{
public void InitializePurchasing(IEnumerable<(string productId, ProductTypeWrapper.ProductType productType)> products);
void BuyProduct(string productId);
decimal GetProductLocalizedPrice(string productId);
string GetProductLocalizedPriceString(string productId);
bool IsInitialized();
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ae2bec11927d49c69e8bb913a0a3c7be
timeCreated: 1709976367

View File

@ -0,0 +1,10 @@
using System;
using System.Threading.Tasks;
namespace WordsToolkit.Scripts.Services.IAP
{
public interface IInitializeGamingServices
{
Task Initialize(Action onSuccess, Action<string> onError);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1a3f2bd448f774ee89a9b4ede6eebcc2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,41 @@
// // ©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.Threading.Tasks;
using Unity.Services.Core;
using Unity.Services.Core.Environments;
using UnityEngine;
using VContainer;
namespace WordsToolkit.Scripts.Services.IAP
{
public class InitializeGamingServices : MonoBehaviour, IInitializeGamingServices
{
private const string k_Environment = "production";
public async Task Initialize(Action onSuccess, Action<string> onError)
{
#if UNITY_PURCHASING
try
{
var options = new InitializationOptions().SetEnvironmentName(k_Environment);
await UnityServices.InitializeAsync(options).ContinueWith(task => onSuccess());
}
catch (Exception exception)
{
onError(exception.Message);
}
#endif
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d070cf48df2d4d3db819987c544aa328
timeCreated: 1650900975

View File

@ -0,0 +1,37 @@
// // ©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 UnityEngine;
namespace WordsToolkit.Scripts.Services.IAP
{
[CreateAssetMenu(fileName = "ProductID", menuName = "WordConnectGameToolkit/IAP/ProductID", order = 0)]
public class ProductID : ScriptableObject
{
public ProductTypeWrapper.ProductType productType;
public string androidId;
public string iosId;
public string ID {
get {
if (Application.platform == RuntimePlatform.Android) {
return androidId;
} else if (Application.platform == RuntimePlatform.IPhonePlayer) {
return iosId;
} else {
return androidId; // existing 'id' field
}
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c45bac7a3df748a6935182751a4577f6
timeCreated: 1725686838

View File

@ -0,0 +1,40 @@
// // ©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.
namespace WordsToolkit.Scripts.Services.IAP
{
/// <summary>
/// Wrapper for the ProductType in case the Unity Purchasing system is not available.
/// </summary>
public class ProductTypeWrapper
{
// Enum definition to be used when the purchasing library is not available.
public enum ProductType
{
Consumable,
NonConsumable,
Subscription
}
#if UNITY_PURCHASING
public static UnityEngine.Purchasing.ProductType GetProductType(ProductType productType)
{
return (UnityEngine.Purchasing.ProductType)(int)productType;
}
#else
public static ProductType GetProductType(ProductType productType)
{
return productType;
}
#endif
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 39ed2a1121a6459a920f914695f83c34
timeCreated: 1726630627