using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor.Callbacks; using UnityEditor.Connect; using UnityEditor.PackageManager; using UnityEditor.PackageManager.Requests; using UnityEngine; using UnityEngine.Purchasing; namespace UnityEditor.Purchasing { /// /// Editor tools to set build-time configurations for app stores. /// [InitializeOnLoad] public static class UnityPurchasingEditor { const string PurchasingPackageName = "com.unity.purchasing"; const string UdpPackageName = "com.unity.purchasing.udp"; const string k_UdpErrorText = "In order to use UDP functionality, you must install or update the Unity Distribution Portal Package. Please configure your project's packages before running UDP-related editor commands in batch mode."; const string ModePath = "Assets/Resources/BillingMode.json"; const string prevModePath = "Assets/Plugins/UnityPurchasing/Resources/BillingMode.json"; static ListRequest m_ListRequestOfPackage; static bool m_UmpPackageInstalled; const string BinPath = "Packages/com.unity.purchasing/Plugins/UnityPurchasing/Android"; const string AssetStoreUdpBinPath = "Assets/Plugins/UDP/Android"; static readonly string PackManUdpBinPath = $"Packages/{UdpPackageName}/Android"; static StoreConfiguration config; static readonly AppStore defaultAppStore = AppStore.GooglePlay; internal delegate void AndroidTargetChange(AppStore store); internal static AndroidTargetChange OnAndroidTargetChange; static readonly bool s_udpAvailable = UdpSynchronizationApi.CheckUdpAvailability(); internal const string MenuItemRoot = "Services/" + PurchasingDisplayName; internal const string PurchasingDisplayName = "In-App Purchasing"; // Check if UDP upm package is installed. internal static bool IsUdpUmpPackageInstalled() { if (m_ListRequestOfPackage == null || m_ListRequestOfPackage.IsCompleted) { return m_UmpPackageInstalled; } else { //As a backup, don't block user if the default location is present. return File.Exists($"Packages/{UdpPackageName}/package.json"); } } static void ListingCurrentPackageProgress() { if (m_ListRequestOfPackage.IsCompleted) { m_UmpPackageInstalled = false; EditorApplication.update -= ListingCurrentPackageProgress; if (m_ListRequestOfPackage.Status == StatusCode.Success) { var udpPackage = m_ListRequestOfPackage.Result.FirstOrDefault(package => package.name == UdpPackageName); m_UmpPackageInstalled = udpPackage != null; } else if (m_ListRequestOfPackage.Status >= StatusCode.Failure) { Debug.LogError(m_ListRequestOfPackage.Error.message); } } } internal static bool IsUdpAssetStorePackageInstalled() { return File.Exists("Assets/UDP/UDP.dll") || File.Exists("Assets/Plugins/UDP/UDP.dll"); } [InitializeOnLoadMethod] static void CheckUdpUmpPackageInstalled() { if (IsInBatchMode()) { CheckUdpUmpPackageInstalledViaManifest(); } else { CheckUdpUmpPackageInstalledViaPackageManager(); } } static bool IsInBatchMode() { return UnityEditorInternal.InternalEditorUtility.inBatchMode; } static void CheckUdpUmpPackageInstalledViaPackageManager() { if (IsInBatchMode()) { Debug.unityLogger.LogIAPError("CheckUdpUmpPackageInstalledViaPackageManager will always fail in Batch Mode. Call CheckUdpUmpPackageInstalledViaManifest instead"); } m_ListRequestOfPackage = Client.List(); EditorApplication.update += ListingCurrentPackageProgress; } static void CheckUdpUmpPackageInstalledViaManifest() { if (!IsInBatchMode()) { Debug.unityLogger.LogIAPWarning("When not running in batch mode, it's more reliable to check the presence of UDP via CheckUdpUmpPackageInstalledViaPackageManager, in case the manifest file is out of date."); } m_UmpPackageInstalled = false; if (File.Exists("Packages/manifest.json")) { var jsonText = File.ReadAllText("Packages/manifest.json"); m_UmpPackageInstalled = jsonText.Contains(UdpPackageName); } } /// /// Since we are changing the billing mode's location, it may be necessary to migrate existing billing /// mode file to the new location. /// [InitializeOnLoadMethod] internal static void MigrateBillingMode() { try { var file = new FileInfo(ModePath); // This will create the new billing file location, if it already exists, this will not do anything. file.Directory.Create(); // See if the file already exists in the new location. if (File.Exists(ModePath)) { return; } // check if the old exists before moving it if (DoesPrevModePathExist()) { AssetDatabase.MoveAsset(prevModePath, ModePath); } } catch (Exception ex) { Debug.LogException(ex); } } internal static bool DoesPrevModePathExist() { return File.Exists(prevModePath); } // Notice: Multiple files per target supported. While Key must be unique, Value can be duplicated! static readonly Dictionary StoreSpecificFiles = new Dictionary() { {"billing-5.2.1.aar", AppStore.GooglePlay}, {"AmazonAppStore.aar", AppStore.AmazonAppStore} }; static readonly Dictionary UdpSpecificFiles = new Dictionary() { { "udp.aar", AppStore.UDP}, { "udpsandbox.aar", AppStore.UDP}, { "utils.aar", AppStore.UDP} }; // Create or read BillingMode.json at Project Editor load static UnityPurchasingEditor() { EditorApplication.delayCall += () => { if (File.Exists(ModePath)) { var oldAppStore = GetAppStoreSafe(); config = StoreConfiguration.Deserialize(File.ReadAllText(ModePath)); if (oldAppStore != config.androidStore) { OnAndroidTargetChange?.Invoke(config.androidStore); } } else { CreateDefaultBillingModeFile(); } }; } static void CreateDefaultBillingModeFile() { TargetAndroidStore(defaultAppStore); } #if !ENABLE_EDITOR_GAME_SERVICES const string SwitchStoreMenuItem = IapMenuConsts.MenuItemRoot + "/Switch Store..."; [MenuItem(SwitchStoreMenuItem, false, 200)] static void OnSwitchStoreMenu() { var window = EditorWindow.GetWindow(typeof(SwitchStoreEditorWindow)); window.titleContent.text = IapMenuConsts.SwitchStoreTitleText; window.minSize = new Vector2(340, 180); window.Show(); GameServicesEventSenderHelpers.SendTopMenuSwitchStoreEvent(); } #else const string SwitchStoreMenuItem = IapMenuConsts.MenuItemRoot + "/Configure..."; #endif private static AppStore GetAppStoreSafe() { var store = AppStore.NotSpecified; if (config != null) { store = config.androidStore; } return store; } /// /// Target a specified Android store. /// This sets the correct plugin importer settings for the store /// and writes the choice to BillingMode.json so the player /// can choose the correct store API at runtime. /// Note: This can fail if preconditions are not met for the AppStore.UDP target. /// /// App store to enable for next build public static void TargetAndroidStore(AppStore target) { TryTargetAndroidStore(target); } internal static AppStore TryTargetAndroidStore(AppStore target) { if (!target.IsAndroid()) { throw new ArgumentException(string.Format("AppStore parameter ({0}) must be an Android app store", target)); } if (target == AppStore.UDP) { if (!s_udpAvailable || (!IsUdpUmpPackageInstalled() && !IsUdpAssetStorePackageInstalled()) || !UdpSynchronizationApi.CheckUdpCompatibility()) { if (IsInBatchMode()) { Debug.unityLogger.LogIAPError(k_UdpErrorText); } else { UdpInstaller.PromptUdpInstallation(); } return ConfiguredAppStore(); } } ConfigureProject(target); SaveConfig(target); OnAndroidTargetChange?.Invoke(target); var targetString = Enum.GetName(typeof(AppStore), target); GenericEditorDropdownSelectEventSenderHelpers.SendIapMenuSelectTargetStoreEvent(targetString); return ConfiguredAppStore(); } // Unfortunately the UnityEditor API updates only the in-memory list of // files available to the build when what we want is a persistent modification // to the .meta files. So we must also rely upon the PostProcessScene attribute // below to process the private static void ConfigureProject(AppStore target) { foreach (var mapping in StoreSpecificFiles) { // All files enabled when store is determined at runtime. var enabled = target == AppStore.NotSpecified; // Otherwise this file must be needed on the target. enabled |= mapping.Value == target; var path = string.Format("{0}/{1}", BinPath, mapping.Key); var importer = (PluginImporter)AssetImporter.GetAtPath(path); if (importer != null) { importer.SetCompatibleWithPlatform(BuildTarget.Android, enabled); } else { // Search for any occurrence of this file // Only fail if more than one found var paths = FindPaths(mapping.Key); if (paths.Length == 1) { importer = (PluginImporter)AssetImporter.GetAtPath(paths[0]); importer.SetCompatibleWithPlatform(BuildTarget.Android, enabled); } } } var UdpBinPath = IsUdpUmpPackageInstalled() ? PackManUdpBinPath : IsUdpAssetStorePackageInstalled() ? AssetStoreUdpBinPath : null; if (s_udpAvailable && !string.IsNullOrEmpty(UdpBinPath)) { foreach (var mapping in UdpSpecificFiles) { // All files enabled when store is determined at runtime. var enabled = target == AppStore.NotSpecified; // Otherwise this file must be needed on the target. enabled |= mapping.Value == target; var path = $"{UdpBinPath}/{mapping.Key}"; var importer = (PluginImporter)AssetImporter.GetAtPath(path); if (importer != null) { importer.SetCompatibleWithPlatform(BuildTarget.Android, enabled); } else { // Search for any occurrence of this file // Only fail if more than one found var paths = FindPaths(mapping.Key); if (paths.Length == 1) { importer = (PluginImporter)AssetImporter.GetAtPath(paths[0]); importer.SetCompatibleWithPlatform(BuildTarget.Android, enabled); } } } } } /// /// To enable or disable importation of assets at build-time, collect Project-relative /// paths matching . /// /// Name of file to search for in this Project /// Relative paths matching public static string[] FindPaths(string filename) { var paths = new List(); var guids = AssetDatabase.FindAssets(Path.GetFileNameWithoutExtension(filename)); foreach (var guid in guids) { var path = AssetDatabase.GUIDToAssetPath(guid); var foundFilename = Path.GetFileName(path); if (filename == foundFilename) { paths.Add(path); } } return paths.ToArray(); } private static void SaveConfig(AppStore enabled) { var configToSave = new StoreConfiguration(enabled); File.WriteAllText(ModePath, StoreConfiguration.Serialize(configToSave)); AssetDatabase.ImportAsset(ModePath); config = configToSave; } internal static AppStore ConfiguredAppStore() { if (config == null) { return defaultAppStore; } return config.androidStore; } // Run me to configure the project's set of Android stores before build [PostProcessScene(0)] internal static void OnPostProcessScene() { if (File.Exists(ModePath)) { try { config = StoreConfiguration.Deserialize(File.ReadAllText(ModePath)); ConfigureProject(config.androidStore); } catch (Exception e) { #if ENABLE_EDITOR_GAME_SERVICES Debug.LogError("Unity IAP unable to strip undesired Android stores from build, check file: " + ModePath); #else Debug.LogError("Unity IAP unable to strip undesired Android stores from build, use menu (e.g. " + SwitchStoreMenuItem + ") and check file: " + ModePath); #endif Debug.LogError(e); } } } [MenuItem(IapMenuConsts.MenuItemRoot + "/Configure...", false, 0)] private static void ConfigurePurchasingSettings() { #if ENABLE_EDITOR_GAME_SERVICES && SERVICES_SDK_CORE_ENABLED var path = PurchasingSettingsProvider.GetSettingsPath(); SettingsService.OpenProjectSettings(path); #elif UNITY_2020_3_OR_NEWER ServicesUtils.OpenServicesProjectSettings(PurchasingService.instance.projectSettingsPath, PurchasingService.instance.settingsProviderClassName); #else EditorApplication.ExecuteMenuItem("Window/General/Services"); #endif GameServicesEventSenderHelpers.SendTopMenuConfigure(); } } }