diff --git a/CHANGELOG-ASSETSTORE.md b/CHANGELOG-ASSETSTORE.md index f7e1983..d68d83f 100644 --- a/CHANGELOG-ASSETSTORE.md +++ b/CHANGELOG-ASSETSTORE.md @@ -29,7 +29,7 @@ - GooglePlay - Missing ProcessPurchase callback at app start when the transaction is (1) purchased, (2) processed by the app with a ProcessPurchaseResult.Pending result, (3) the app is terminated, and (4) the app is restarted - GooglePlay - NullReferenceException from "FillPurchases" (logged internal API) when returning from background, unpredictably - Apple - Unity IAP 2.2.2's Apple Silicon fix not included in release; continuous integration pipeline fixed -- `StandardPurchasingModule.appStore` returns `AppStore.MacAppStore` for Mac App Store, `AppStore.AppleAppStore` for iOS App Store, and `AppStore.WinRT` for Windows Desktop. (No change to +- `StandardPurchasingModule.appStore` returns `AppStore.MacAppStore` for Mac App Store, `AppStore.AppleAppStore` for iOS App Store, and `AppStore.WinRT` for Windows Desktop. (No change to `AppStore.SamsungApps`, `AppStore.AmazonAppStore`, or `AppStore.GooglePlay`.) ## [2.2.4] - 2020-12-03 @@ -38,7 +38,7 @@ - GooglePlay - `IStoreListener.ProcessPurchase` called more than once for any purchase which is not consumed, i.e. when `ProcessPurchaseResult.Pending` is returned, by fixing a race-condition. ### Changed -- GooglePlay - To receive `ProcessPurchase` calls after foregrounding the app, when a purchase is made outside the app (e.g. in the Play Store app), please upgrade the core package via the Package Manager to `com.unity.purchasing@2.2.1` or higher. +- GooglePlay - To receive `ProcessPurchase` calls after foregrounding the app, when a purchase is made outside the app (e.g. in the Play Store app), please upgrade the core package via the Package Manager to `com.unity.purchasing@2.2.1` or higher. ## [2.2.3] - 2020-12-01 @@ -63,13 +63,13 @@ ## [2.2.1] - 2020-11-13 ### Fixed -- GooglePlay - ProductMetadata.localizedPrice always `0` +- GooglePlay - ProductMetadata.localizedPrice always `0` - GooglePlay - "Main" thread warning seen in IStoreListener.OnInitialized and related callbacks. ### Added - GooglePlay - Subscription metadata is now available in `GoogleProductMetadata` from `ProductMetadata.GetGoogleProductMetadata()` via `IStoreController.products`. - For example, use `GoogleProductMetadata googleMetadata = storeController.product.all[0].metadata.GetGoogleProductMetadata();` now instead of the deprecated, `IGooglePlayStoreExtensions.GetProductJSONDictionary`. - - string originalJson - Note, a single SkuDetails JSON, instead of multiple from `GetProductJSONDictionary` + - string originalJson - Note, a single SkuDetails JSON, instead of multiple from `GetProductJSONDictionary` - string subscriptionPeriod - string freeTrialPeriod - string introductoryPrice @@ -105,14 +105,14 @@ - IAP Catalog - GooglePlay - pricing template when exporting to CSV, now sets autofill pricing to `false` instead of `true` - GooglePlay - Subscription receipts will update, e.g. after an upgrade or downgrade, whenever the player pauses or resumes their app. See this change reflected in the `product.receipt` of `IStoreController.products`. -### Added +### Added - Apple Macos - Support for building IL2CPP on MacOS ## [2.1.1] - 2020-10-23 ### Fixed - Amazon - Fix build failure caused by duplicate classes -- Amazon - Fix ResponseReceiver flaw reported by Amazon APK audit caused by permission attribute location in AndroidManifest.xml +- Amazon - Fix ResponseReceiver flaw reported by Amazon APK audit caused by permission attribute location in AndroidManifest.xml ## [2.1.0] - 2020-10-14 ### Future @@ -122,8 +122,8 @@ - GooglePlay - Live payments using `aggressivelyRecoverLostPurchases = true` - switched to Google's Purchase Token from using Google's Order ID to represent all transaction IDs. Automatically sets `Product.transactionID` to GooglePlay `purchaseToken` when `aggressivelyRecoverLostPurchases` is `true`. Continues to use `orderId`, otherwise. CLARIFICATION: To reinforce the preferred usage of `aggressivelyRecoverLostPurchases`, a de-duplicating backend purchase verification server is recommended to be added to a game's transaction verification pipeline when using this feature. Without such a server the recovered purchases may not be easily or safely de-duplicated by a client. - When upgrading from previous versions of Unity IAP, and if enabled `bool IGooglePlayConfiguration.aggressivelyRecoverLostPurchases`, any purchases your users have made will be processed again by Unity IAP; ProcessPurchase will be called for all these purchases. - - Afterwards, for future purchases, this change reduces future duplicate processing calls; ProcessPurchase should no longer be called after an interrupted purchase for an item already purcahsed by the user. - - Update purchase verification servers to treat orderId and purchaseToken as the same ID. Extract the purchaseToken from the JSON receipt, or fetch the orderId using a purchaseToken and the server API [`purchases.products` Google Play Developer API](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products). + - Afterwards, for future purchases, this change reduces future duplicate processing calls; ProcessPurchase should no longer be called after an interrupted purchase for an item already purcahsed by the user. + - Update purchase verification servers to treat orderId and purchaseToken as the same ID. Extract the purchaseToken from the JSON receipt, or fetch the orderId using a purchaseToken and the server API [`purchases.products` Google Play Developer API](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products). - Override this behavior with `UsePurchaseTokenForTransactionId`, below. - GooglePlay Security - Add GooglePlayReceipt.orderID and obsolete GooglePlayReceipt.transactionID for the local receipt validator, for clarity. - GooglePlay - Reduce the frequency of double-processing when a purchase is canceled, if using `aggressivelyRecoverLostPurchases = true`. Always records purchaseToken in TransactionLog when a transaction is completed. @@ -131,9 +131,9 @@ ### Added - GooglePlay - Ability to override the `Product.transactionID` and use either Google's Purchase Token, or the legacy Order ID when possible, with `UsePurchaseTokenForTransactionId`. - Call `void IGooglePlayConfiguration.UsePurchaseTokenForTransactionId(bool usePurchaseToken)` to disable the default behavior - - a) `false` to use the `orderId`, when possible; this is the legacy and non-unique transaction ID behavior. This must switch to the purchaseToken when the orderId is not available from Google, which is when `aggressivelyRecoverLostPurchases = true`. + - a) `false` to use the `orderId`, when possible; this is the legacy and non-unique transaction ID behavior. This must switch to the purchaseToken when the orderId is not available from Google, which is when `aggressivelyRecoverLostPurchases = true`. - b) `true` to always use the unique purchaseToken in transactionID. NOTE: this is the preferred option, and it will be the only behavior available in a future version of Unity IAP. - - **Background:** The GooglePlay purchaseToken is the unique identifier for all GooglePlay purchases; it is always available for all purchase types and all purchase records. The GooglePlay orderId is not available for all purchase types, notably sandbox and promo code, and also is currently not available for those purchase records which are returned for `aggressivelyRecoverLostPurchases = true` recovered purchases: the Google Play Billing history API used by this feature does not return orderId and therefore cannot be set as the Product.transactionID at that time. Historically, Unity IAP chose to prefer orderId for transactionID in its original implementation. And when the orderId was missing from purchase records (sandbox test purchases, and other no-money purchases), Unity IAP would use the purchaseToken as the transactionID. + - **Background:** The GooglePlay purchaseToken is the unique identifier for all GooglePlay purchases; it is always available for all purchase types and all purchase records. The GooglePlay orderId is not available for all purchase types, notably sandbox and promo code, and also is currently not available for those purchase records which are returned for `aggressivelyRecoverLostPurchases = true` recovered purchases: the Google Play Billing history API used by this feature does not return orderId and therefore cannot be set as the Product.transactionID at that time. Historically, Unity IAP chose to prefer orderId for transactionID in its original implementation. And when the orderId was missing from purchase records (sandbox test purchases, and other no-money purchases), Unity IAP would use the purchaseToken as the transactionID. - **Impact:** Since Unity IAP version 1.23.3 this resulted in non-unique transactionIDs for purchases made which were "aggressively" restored: two distinct ProcessPurchase calls for one purchase could be generated, where Product.transactionID would change between two values (orderId and purchaseToken) and be non-unique. With this version, Unity IAP is starting a transition to only using purchaseToken, and avoids the impact for any new purchases made by a user. ### Fixed @@ -159,7 +159,7 @@ ### Changed - IAP Updater "Updater Settings..." button now reads "More Information..." to be more accurate -- UDP store implementation is still available, but must be installed in a separate module, available in either Asset Store or Package manager. The UDP module will need to be updated manually. (See above) +- UDP store implementation is still available, but must be installed in a separate module, available in either Asset Store or Package manager. The UDP module will need to be updated manually. (See above) - Visibility of INativeStores is now public, mainly to support the new UDP package's needs - Resource files - The Product Catalog, Android Target Billing Mode, and Receipt Obfuscator Tangle files will be moved out of the plugins folder. @@ -189,17 +189,17 @@ ### Added - GooglePlay - Improves the chance of successfully purchasing a Consumable or NonConsumable when the _purchase flow_ is interrupted. Also addresses the dialog, "Your order is still being processed". - Unity IAP will now detect this _purchasing_ failure. It will call the `IStoreListener.OnPurchaseFailed` API, initially. Then it will query Google Play for purchase success during the current app session until network is restored, and it will continue querying in the next app session, after a restart. It will finally call the `IStoreListener.ProcessPurchase` API if it finds a successful, unaccounted purchase. - - Addresses the case where (1) a consumable or nonconsumable purchase flow is started and (2) a network disruption occurs, or the app is sent to the background and the purchasing Activity is canceled, or the app is terminated. + - Addresses the case where (1) a consumable or nonconsumable purchase flow is started and (2) a network disruption occurs, or the app is sent to the background and the purchasing Activity is canceled, or the app is terminated. - GooglePlay - Improves the chance of successfully repurchasing a Consumable whose successful transaction failed however to be _completed_ during the current app session. - Unity IAP will now detect this _consumption_ failure. It will automatically retry completing the purchase until it succeeds. Note that `DuplicateTransaction` may still be reported while the retry is ongoing, until the user's product is repurchasable again. See below for new APIs to monitor the consumption flow. - Addresses the case where (1) a Consumable purchase calls `IStoreListener.ProcessPurchase`, then (2) the transaction is completed by returning `ProcessPurchaseResult.Complete` from `IStoreListener.ProcessPurchase` or by directly calling `IStoreController.ConfirmPendingPurchase` [internally this always records the transaction identifier to the TransactionLog], and finally (3) an interruption (network or exit) aborts the transaction consumption. Only restarting the app or refunding the purchase would reliably resolve this case. -- GooglePlay - Adds an `"isOwned" : ` sub-entry to the `Product.receipt`'s `"Payload"` JSON entry in order to help developers understand this product's current ownership state. +- GooglePlay - Adds an `"isOwned" : ` sub-entry to the `Product.receipt`'s `"Payload"` JSON entry in order to help developers understand this product's current ownership state. - Contains `true` if the product is owned by the user. And please note that `true` may also indicate that Unity IAP is actively retrying consumption. Its boolean value will be `false` if the product is available for repurchase, or if we do not yet know Google Play's current status for this product. To clarify the receipt structure, `"isOwned"` is located in the Google Play-specific escaped-JSON sub-document. Sample `Product.receipt`, abbreviated: `{"Payload":"{\"json\": ..., \"signature\": ..., \"isOwned\":true}}"`. See the Google Play section of the [Unity IAP Receipt receipt documentation](https://docs.unity3d.com/Manual/UnityIAPPurchaseReceipts.html) for more on the receipt JSON structure. -- GooglePlay - Adds `boolean IGooglePlayStoreExtensions.IsOwned(Product)` API to conveniently extract the new ownership state, above, from the Google Play JSON receipt. - - Returns `true` if the product is still owned by the user. Returns `false` if the product is available for repurchase. Example: +- GooglePlay - Adds `boolean IGooglePlayStoreExtensions.IsOwned(Product)` API to conveniently extract the new ownership state, above, from the Google Play JSON receipt. + - Returns `true` if the product is still owned by the user. Returns `false` if the product is available for repurchase. Example: ```extensionProvider.GetExtension()``` ```.IsOwned(storeController.products.WithID("100.gold.coins"));```. -- GooglePlay - Adds `void IGooglePlayStoreExtensions.SetLogLevel(int level)` API to reduce logging. +- GooglePlay - Adds `void IGooglePlayStoreExtensions.SetLogLevel(int level)` API to reduce logging. - `level` defaults to the legacy value of `0` and configures the Google Play Java store integration to emit debug, info, warning, and error logs. Setting `1` will restrict logging to emit only warnings and errors. Example: `extensionProvider.GetExtension().SetLogLevel(1)`. - GooglePlay - After the purchasing dialog, "You already own this product" from Google Play is shown, the `IStoreListener.OnPurchaseFailed` API is calls with an error of `PurchaseFailureReason.DuplicateTransaction`. - Unity IAP now treats "You already own this product" as a successful purchase, and _also_ calls `IStoreListener.ProcessPurchase`. Note: This amends the related behavior introduced in 1.23.1. @@ -232,7 +232,7 @@ ### Fixed - GooglePlay - SubscriptionInfo.getSubscriptionInfo() KeyNotFoundException when parsing receipts which omit expected fields. - GooglePlay - IStoreListener.OnInitializeFailed / IStoreCallback.OnSetupFailed should return InitializationFailureReason.AppNotKnown error when user changes password off-device - user must login. Previously erroneously generated infinite error 6 codes when fetching purchase history after password change. -- OverflowException when initializing if device locale used the comma (“,”) character as decimal separator. +- OverflowException when initializing if device locale used the comma (“,”) character as decimal separator. ## [1.22.0] - 2019-03-18 ### Added @@ -258,12 +258,12 @@ - Added a callback function that allows developers to check the state of the upgrade/downgrade process of subscriptions on GooglePlay. ### Fixed -- Google Daydream - Correctly Displays IAP Prompt in 3d VR version instead of native 2D. +- Google Daydream - Correctly Displays IAP Prompt in 3d VR version instead of native 2D. - Fixed issue where IAP catalog prevented deletion of Price under Google Configuration. - Amazon Store - Fixed bug where Amazon store could not correctly parse currencies for certain countries. - MacOS - Fixed bug that causes non-consumables to auto-restore on MacOS apps after re-install, instead of requiring the the Restore button to be clicked. - Updated Android Response Code to return correct message whenever an activity is cancelled. -- Fixed Mono CIL linker error causing initialization failure in Unity 5.3 +- Fixed Mono CIL linker error causing initialization failure in Unity 5.3 - Fixed inefficient Apple Receipt Parser that was slowing down when a large number of transactions were parsed on auto-restore. ## [1.20.0] - 2018-06-29 @@ -271,20 +271,20 @@ - API for developers to check SkuDetails for all GooglePlay store products, including those that have not been purchased. - Error Code Support for Amazon. - Support upgrade/downgrade Subscription Tiers for GooglePlayStore. -- Support Subscription status check (valid/invalid) for Amazon Store. +- Support Subscription status check (valid/invalid) for Amazon Store. ### Changed - Location of Product Catalog from Assets/Plugins/UnityPurchasing/Resources folder to Assets/Resources. - Amazon Receipt with enriched product details and receipt details. ### Fixed -- Issue where Unknown products (including non-consumables) were consumed during initialization. +- Issue where Unknown products (including non-consumables) were consumed during initialization. - ArgumentException where currency was set to null string when purchase was made. ## [1.19.0] - 2018-04-17 ### Added - For GooglePlay store, `developerPayload` has been encoded to base64 string and formatted to a JSON string with two other information of the product. When extract `developerPayload` from the product receipt, firstly decode the json string and get the `developerPayload` field base64 string, secondly decode the base64 string to the original `developerPayload`. -- `SubscriptionManager` - This new class allows developer to query the purchased subscription product's information. (available for AppleStore and GooglePlay store) +- `SubscriptionManager` - This new class allows developer to query the purchased subscription product's information. (available for AppleStore and GooglePlay store) - For GooglePlay store, this class can only be used on products purchased using IAP 1.19.0 SDK. Products purchased on previous SDKs do not have the fields in the "developerPayload" that are needed to parse the subscription information. - If the "Payload" json string field in the product's json string receipt has a "skuDetails" filed, then this product can use `SubscriptionManager` to get its subscription information. - Added the `StoreSpecificPurchaseErrorCode` enum. Currently contains values for all Apple and Google Play error codes that are returned directly from the store. @@ -325,7 +325,7 @@ - Version Log - Changed logging of Unity IAP version (e.g. "1.15.0") to be only at runtime and not while in the Editor ### Fixed -- Facebook - Correctly handles situations where the number of available products exceeds the Facebook server response page size +- Facebook - Correctly handles situations where the number of available products exceeds the Facebook server response page size - Updater will no longer prompt for updates when Unity is running in batch mode - Gradle - Include and relocate sample Proguard configuration file to Assets/Plugins/UnityPurchasing/Android/proguard-user.txt.OPTIONAL.txt; was missing from 1.13.2 - Security - Upgrades project to autogenerate UnityChannelTangle class if missing when GooglePlayTangle obfuscated secret receipt validation support class is present. @@ -342,7 +342,7 @@ ## [1.14.0] - 2017-09-18 ### Added - Codeless IAP - Added an `IAPListener` Component to extend Codeless IAP functionality. Normally with Codeless IAP, purchase events are dispatched to an `IAPButton` UI Component that is associated with a particular product. The `IAPListener` does not show any UI. It will receive purchase events that do not correspond to any active `IAPButton`. - - The active `IAPListener` is a fallback—it will receive any successful or failed purchase events (calls to `ProcessPurchase` or `OnPurchaseFailed`) that are _not_ handled by an active Codeless `IAPButton` Component. + - The active `IAPListener` is a fallback—it will receive any successful or failed purchase events (calls to `ProcessPurchase` or `OnPurchaseFailed`) that are _not_ handled by an active Codeless `IAPButton` Component. - When using the `IAPListener`, you should create it early in the lifecycle of your app, and not destroy it. By default, it will set its `GameObject` to not be destroyed when a new scene is loaded, by calling `DontDestroyOnLoad`. This behavior can be changed by setting the `dontDestroyOnLoad` field in the Inspector. - If you use an `IAPListener`, it should be ready to handle purchase events at any time, for any product. Promo codes, interrupted purchases, and slow store behavior are only a few of the reasons why you might receive a purchase event when you are not showing a corresponding `IAPButton` to handle the event. - Example use: If a purchase is completed successfully with the platform's app store but the user quits the app before the purchase is processed by Unity, Unity IAP will call `ProcessPurchase` the next time it is initialized—typically the next time the app is run. If your app creates an `IAPListener`, the `IAPListener` will be available to receive this `ProcessPurchase` callback, even if you are not yet ready to create and show an `IAPButton` in your UI. @@ -386,21 +386,21 @@ ```csharp public void OnInitialized(IStoreController controller, IExtensionProvider extensions) { - // Set the order of the promoted items - var appleExtensions = extensions.GetExtension(); - appleExtensions.SetStorePromotionOrder(new List{ - controller.products.WithID("sword"), - controller.products.WithID("subscription") - }); + // Set the order of the promoted items + var appleExtensions = extensions.GetExtension(); + appleExtensions.SetStorePromotionOrder(new List{ + controller.products.WithID("sword"), + controller.products.WithID("subscription") + }); } ``` ```csharp public void OnInitialized(IStoreController controller, IExtensionProvider extensions) { - // Set the visibility of promoted items - var appleExtensions = extensions.GetExtension(); - appleExtensions.SetStorePromotionVisibility(controller.products.WithID("subscription"), AppleStorePromotionVisibility.Hide); - appleExtensions.SetStorePromotionVisibility(controller.products.WithID("100.gold.coins"), AppleStorePromotionVisibility.Default); + // Set the visibility of promoted items + var appleExtensions = extensions.GetExtension(); + appleExtensions.SetStorePromotionVisibility(controller.products.WithID("subscription"), AppleStorePromotionVisibility.Hide); + appleExtensions.SetStorePromotionVisibility(controller.products.WithID("100.gold.coins"), AppleStorePromotionVisibility.Default); } ``` @@ -506,7 +506,7 @@ module.useFakeStoreAlways = true; ### Fixed - Editor - checkmarks refresh for Targeted Android Store after Editor Play/Stop - Editor - hides spurious Component MenuItems -- Linux Editor - BillingMode.json path case-sensitivity +- Linux Editor - BillingMode.json path case-sensitivity - IAP Catalog - clearer text for Export button: "App Store Export" ## [1.9.2] - 2016-11-29 @@ -624,10 +624,10 @@ using UnityEditor; // A sample Editor script. public class MyEditorScript { - void AnEditorMethod() { - // Set the store to Google Play. - UnityPurchasingEditor.TargetAndroidStore(AndroidStore.GooglePlay); - } + void AnEditorMethod() { + // Set the store to Google Play. + UnityPurchasingEditor.TargetAndroidStore(AndroidStore.GooglePlay); + } } ``` @@ -719,4 +719,4 @@ string receipt = builder.Configure ().appReceipt; - Google Play - Apple App Store - Mac App Store -- Windows Store (Universal) \ No newline at end of file +- Windows Store (Universal) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bcdbbb..1e199da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,170 @@ # Changelog +## [4.8.0] - 2023-04-12 +### Added +- Added new [IAP Button](https://docs.unity3d.com/Packages/com.unity.purchasing@4.8/manual/CodelessIAPButton.html) in the editor. This new button allows for more UI customization. The new button will no longer update the button fields by default. +- Added a new event `OnProductFetched(Product)` to the [IAP Listener](https://docs.unity3d.com/Packages/com.unity.purchasing@4.8/manual/IAPListener.html) and to the [IAP Button](https://docs.unity3d.com/Packages/com.unity.purchasing@4.8/manual/CodelessIAPButton.html) it is called after fetching products from the app stores. +- Added a new `OnPurchaseFailed(Product, PurchaseFailureDescription)` callback containing more information on the failed purchase in `IDetailedStoreListener : IStoreListener` + +### Changed +- [IAP Button](https://docs.unity3d.com/Packages/com.unity.purchasing@4.8/manual/IAPButton.html) is now obsolete. +- Google Play - Billing Library update from version 4.0.0 to 5.1.0 [Google Release Notes](https://developer.android.com/google/play/billing/release-notes). + New Google Billing features are not supported yet, they will be included in a future major update. +- Removed the nullable operator `?` from public interfaces and classes. +- `IStoreListener.OnPurchaseFailed` is now obsolete. +- When present, Analytics 4.4.0 and above will now use the new interface `IAnalyticsStandardEventComponent` from Services Cores 1.8.1. +- Upgraded `com.unity.services.core` from 1.5.2 to 1.8.1. + +### Fixed +- Samples - Some samples had IAP 4.6.0 `IStoreListener` changes not completely implemented causing compilation errors. + +## [4.7.0] - 2023-02-09 +### Added +- Added `storeSpecificErrorCode` to `PurchaseFailureDescription.message` when available. + +### Fixed +- Unity IAP will consider the call to `UnityPurchasing.initialize` completed before invoking the correct callback `IStoreListener.OnInitialized` or `IStoreListener.OnInitializeFailed`. This prevents these callbacks from being invoked more than once per initialization. +- GooglePlay - Fixed `No such proxy method` exception in our representation of `BillingClientStateListener.onBillingServiceDisconnected()` introduced by Unity IAP 4.6.0 +- Apple - Fixed a `NullReferenceException` happening when the receipt isn't found. + +### Changed +- Removed `com.unity.services.analytics` from the IAP SDK dependencies + +## [4.6.0] - 2023-02-02 +### Added +- Added a new restore transaction callback `RestoreTransactions(Action callback)` to obtain the error string when RestoreTransactions is not successful (`IAppleExtensions` and `IGooglePlayStoreExtensions`). +- Added a new initialize failed callback `IStoreListener.OnInitializeFailed(InitializationFailureReason, string)` to obtain the error string when OnInitializeFailed is invoked. +- Added a new setup failed callback `IStoreCallback.OnSetupFailed(InitializationFailureReason, string)` to obtain the error string when OnSetupFailed is invoked. +- Added a new FetchAdditionalProducts. The failCallback contains an error string. `IStoreController.FetchAdditionalProducts(HashSet, Action, Action)` +- Apple - `Product.appleOriginalTransactionId` : Returns the original transaction ID. This field is only available when the purchase was made in the active session. +- Apple - `Product.appleProductIsRestored` : Indicates whether the product has been restored. +- GooglePlay - `IGooglePlayConfiguration.SetFetchPurchasesExcludeDeferred(bool exclude)` has been added to revert to the previous behaviour. This is not recommended and should only be used if `Deferred` purchases are handled in your `IStoreListener.ProcessPurchase`. +- GooglePlay - `IGooglePlayStoreExtensions.GetPurchaseState(Product product)` has been added to obtain the `GooglePurchaseState` of a product. +- GooglePlay - Added missing values to `GoogleBillingResponseCode` in order to output it in `PurchaseFailureDescription`'s message when available. +- Codeless - Added to the [IAP Button](https://docs.unity3d.com/Packages/com.unity.purchasing@4.6/manual/IAPButton.html) the option to add a script for the On Transactions Restored: `void OnTransactionsRestored(bool success, string? error)` + +### Changed +- Upgraded `com.unity.services.core` from 1.3.1 to 1.5.2 +- Upgraded `com.unity.services.analytics` from 4.0.1 to 4.2.0 +- The old OnInitializeFailed `OnInitializeFailed(InitializationFailureReason error)` was marked `Obsolete` +- The old OnSetupFailed `OnSetupFailed(InitializationFailureReason reason)` was marked `Obsolete` +- The old FetchAdditionalProducts `FetchAdditionalProducts(HashSet additionalProducts, Action successCallback, Action failCallback)` was marked `Obsolete` +- The old restore transaction callback `RestoreTransactions(Action callback)` was marked `Obsolete` (`IAppleExtensions` and `IGooglePlayStoreExtensions`). +- Apple - Transactions received from Apple that are invalid (where the product is not entitled) will no longer output the `Finishing transaction` log. This only affects transactions that never reached the `ProcessPurchase`. +- GooglePlay - The enum `GooglePurchaseState` now recognizes `4` as `Deferred`. + +### Fixed +- Analytics - A ServicesInitializationException introduced in Analytics 4.3.0 is now handled properly. +- Analytics - Fixed an issue where transactions events were invalidated when there was no localization data for a product. +- GooglePlay - Fixed a `NullReferenceException` when querying sku details while the BillingClient is not ready. +- GooglePlay - Fixed [Application Not Responding (ANR)](https://developer.android.com/topic/performance/vitals/anr) when foregrounding the application while disconnected from the Google Play Store. +- GooglePlay - Limited the occurence of `PurchasingUnavailable` errors when retrieving products while in a disconnected state to once per connection. +- GooglePlay - `Deferred` purchases are, by default, no longer sent to `IStoreListener.ProcessPurchase` when fetching purchases. This avoids the possibility of granting products that were not paid for. These purchases will only be processed once they become `Purchased`. This can be reverted with `IGooglePlayConfiguration.SetFetchPurchasesExcludeDeferred(bool exclude)` to not exclude them, but `Deferred` purchases will need to be handled in `IStoreListener.ProcessPurchase`. +- Unity IAP will consider the call to `UnityPurchasing.initialize` completed before invoking the correct callback `IStoreListener.OnInitialized` or `IStoreListener.OnInitializeFailed`. This prevents these callbacks from being invoked more than once per initialization. + +## [4.5.2] - 2022-10-28 +### Fixed +- Removed unused exception variable causing a compiler warning CS0168. +- Telemetry - Calls to telemetry reporting were occasionally tripping a `NullReferenceException`, `IndexOutOfRangeException` or `KeyNotFoundException`, for some users. These exceptions are now caught safely and logged. These failures are also mitigated by moving all Telemetry calls to the main thread. Issue noticed in IAP 4.4.1, but may be older. +- Apple - Optimized memory usage when processing transactions to prevent out of memory crash when processing transactions on launch. +- Batch Mode - Calls to `UnityPurchasingEditor.TargetAndroidStore` to select UDP will now successfully check UDP package installation and log an error instead of throwing a blocking popup when executed as part of a Batch Mode command. +- Analytics - Removed escape characters for receipt JSON which was causing parsing issues in the backend. +- GooglePlay - Fixed a bug causing a crash when retrying to finish a transaction while disconnected + +## [4.5.1] - 2022-10-13 +### Fixed +- GooglePlay - Fixed deferred purchases being processed when the app is foregrounded. Issue introduced in Unity IAP 4.5.0. +- GooglePlay - Fixed a `NullReferenceException` in `DequeueQueryProducts` happening when launching the app. Issue introduced in Unity IAP 4.2.0. +- Analytics - Fixed a `NullReferenceException` when reporting failed transactions of purchase unavailable products. Issue introduced in Unity IAP 4.2.0. +- Analytics - Legacy Analytics will no longer report events in custom UGS environments, which would cause misreported live sales figures. Issue introduced in Unity IAP 4.2.0. + +## [4.5.0] - 2022-09-23 +### Added +- Apple - Add support for [Family Sharing](https://developer.apple.com/app-store/subscriptions/#family-sharing). + - API `IAppleConfiguration.SetEntitlementsRevokedListener(Action>` called when entitlement to a products are revoked. The `Action` will be called with the list of revoked products. See documentation "Store Guides" > "iOS & Mac App Stores" for a sample usage. + - API - Product metadata is now available in `AppleProductMetadata` from `ProductMetadata.GetAppleProductMetadata()` via `IStoreController.products`. + - API `AppleProductMetadata.isFamilyShareable` indicated if the product is family shareable. + - `Apple App Store - 11 Family Sharing` sample that showcases how to use Unity IAP to manage family shared purchases. + +### Fixed +- GooglePlay - Processing out-of-app purchases such as Promo codes no longer requires the app to be restarted. The + purchase will be processed the next time the app is foregrounded. Technical limitation: In the case of promo codes, if + the app is opened while the code is redeemed, you might receive an additional call + to `IStoreListener.OnPurchaseFailed` with `PurchaseFailureReason.Unknown`. This can be safely ignored. +- GooglePlay - Fixed a `NullReferenceException` that would rarely occur when retrieving products due to a concurrency issue introduced in Unity IAP 4.2.0 + +## [4.4.1] - 2022-08-11 +### Fixed +- GooglePlay - Fixed NullReferenceException and ArgumentException that would rarely occur due to a concurrency issue introduced in Unity IAP 4.2.0 +- Amazon - Set android:export to true to support Android API level 31+ + +## [4.4.0] - 2022-07-11 +### Added +- GooglePlay - Google Play Billing Library version 4.0.0. + - The Multi-quantity feature is not yet supported by the IAP package and will come in a future update. **Do not enable `Multi-quantity` in the Google Play Console.** + - Add support for + the [IMMEDIATE_AND_CHARGE_FULL_PRICE](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode#IMMEDIATE_AND_CHARGE_FULL_PRICE) + proration mode. Use `GooglePlayProrationMode.ImmediateAndChargeFullPrice` for easy access. + - The `"skuDetails"` in the receipt json is now an array of the old structure, not just one object. It will only have one element in most cases, so if this is being parsed in your app, treat it like an array and get the first element by default. + +### Fixed +- GooglePlay - Fix `IGooglePlayConfiguration.SetDeferredPurchaseListener` + and `IGooglePlayConfiguration.SetDeferredProrationUpgradeDowngradeSubscriptionListener` callbacks sometimes not being + called from the main thread. +- GooglePlay - When configuring `IGooglePlayConfiguration.SetQueryProductDetailsFailedListener(Action retryCount)`, the action will be invoked with retryCount starting at 1 instead of 0. +- GooglePlay - Added a validation when upgrading/downgrading a subscription that calls `IStoreListener.OnPurchaseFailed` with `PurchaseFailureReason.ProductUnavailable` when the old transaction id is empty or null. This can occur when attempting to upgrade/downgrade a subscription that the user doesn't own. + +## [4.3.0] - 2022-06-16 +### Added +- GooglePlay - API `IGooglePlayConfiguration.SetQueryProductDetailsFailedListener(Action)` called when Unity IAP fails to query product details. The `Action` will be called on each query product details failure with the retry count. See documentation "Store Guides" > "Google Play" for a sample usage. + +## [4.2.1] - 2022-06-14 +### Fixed +- Downgrade `com.unity.services.core` from 1.4.1 to 1.3.1 due to a new bug found in 1.4.1 + +## [4.2.0] - 2022-06-13 + +### Added +- Feature to automatically initialize **Unity Gaming Services** through the catalog UI. Please see the [documentation](https://docs.unity3d.com/Packages/com.unity.purchasing@4.2/manual/UnityIAPInitializeUnityGamingServices.html) for more details. + +### Changed +- The In-App Purchasing package now requires **Unity Gaming Services** to have been initialized before it can be used. +For the time being **IAP** will continue working as usual, but will log a warning if **Unity Gaming Services** has not been initialized. +In future releases of this package, initializing **Unity Gaming Services** will be mandatory. Please see the [documentation](https://docs.unity3d.com/Packages/com.unity.purchasing@4.2/manual/UnityIAPInitializeUnityGamingServices.html) for more details. + +## [4.2.0-pre.2] - 2022-04-28 + +### Added +- Support for Unity Analytics TransactionFailed event. +- Sample showcasing how to initialize [Unity Gaming Services](https://unity.com/solutions/gaming-services) using the [Services Core API](https://docs.unity.com/ugs-overview/services-core-api.html) + +### Changed +- The Analytics notice in the In-App Purchasing service window has been removed for Unity Editors 2022 and up. + +## [4.2.0-pre.1] - 2022-04-07 + +### Added +- Support for the [new Unity Analytics](https://unity.com/products/unity-analytics) [transaction event](https://docs.unity.com/analytics/AnalyticsSDKAPI.html#Transaction). +- The package will now send telemetry diagnostic and metric events to help improve the long-term reliability and performance of the package. + +### Changed +- The minimum Unity Editor version supported is 2020.3. +- The In-App Purchasing service window now links to the [new Unity Dashboard](https://dashboard.unity3d.com/) for Unity Editors 2022 and up. + +### Fixed +- GooglePlay - Fixed OnInitializeFailed never called if GooglePlay BillingClient is not ready during initialization. +- GooglePlay - GoogleBilling is allowed to initialize correctly even if the user's Google account is logged out, so long as it is linked. The user will need to log in to their account to continue making purchases. +- Fixed a build error `DirectoryNotFoundException` that occurred when the build platform was iOS or tvOS and the build target was another platform. + +## [4.1.5] - 2022-05-17 + +### Fixed +- GooglePlay - Fixed a null reference exception introduced in Unity IAP 4.1.4 that could happen when cancelling an in-app purchase. + ## [4.1.4] - 2022-03-30 ### Fixed -- GooglePlay - Fixed issue where if an app is backgrounded while a purchase is being processed, +- GooglePlay - Fixed issue where if an app is backgrounded while a purchase is being processed, an `OnPurchaseFailed` would be called with the purchase failure reason `UserCancelled`, even if the purchase was successful. ## [4.1.3] - 2022-01-11 @@ -39,7 +200,7 @@ an `OnPurchaseFailed` would be called with the purchase failure reason `UserCanc - Fixed warning, missing await for async call in ExponentialRetryPolicy.cs ### Removed -- Removed the original and complex Unity IAP sample known as "Example", or "IAP Demo". Please use the recently added [samples](https://docs.unity3d.com/Packages/com.unity.purchasing@4.0/manual/Overview.html#learn-more) for a granular introduction to In-App Purchasing features. +- Removed the original and complex Unity IAP sample known as "Example", or "IAP Demo". Please use the recently added [samples](https://docs.unity3d.com/Packages/com.unity.purchasing@4.0/manual/Overview.html#learn-more) for a granular introduction to In-App Purchasing features. ## [4.0.3] - 2021-08-18 ### Added @@ -70,7 +231,7 @@ an `OnPurchaseFailed` would be called with the purchase failure reason `UserCanc ## [4.0.0] - 2021-07-19 ### Added -- Codeless Listener method to access the store configuration after initialization. +- Codeless Listener method to access the store configuration after initialization. - `CodelessIAPStoreListener.Instance.GetStoreConfiguration` - Several samples to the [Package Manager Details view](https://docs.unity3d.com/Manual/upm-ui-details.html) for com.unity.purchasing: - Fetching additional products @@ -93,13 +254,13 @@ an `OnPurchaseFailed` would be called with the purchase failure reason `UserCanc - Reorganized and renamed APIs: - `CodelessIAPStoreListener.Instance.ExtensionProvider.GetExtension` to `CodelessIAPStoreListener.Instance.GetStoreExtensions` to match the new `GetStoreConfiguration` API, above - `IGooglePlayStoreExtensions.NotifyDeferredProrationUpgradeDowngradeSubscription` to `IGooglePlayConfiguration.NotifyDeferredProrationUpgradeDowngradeSubscription` - - `IGooglePlayStoreExtensions.NotifyDeferredPurchase` to `IGooglePlayConfiguration.NotifyDeferredPurchase` - - `IGooglePlayStoreExtensions.SetDeferredProrationUpgradeDowngradeSubscriptionListener` to `IGooglePlayConfiguration.SetDeferredProrationUpgradeDowngradeSubscriptionListener` + - `IGooglePlayStoreExtensions.NotifyDeferredPurchase` to `IGooglePlayConfiguration.NotifyDeferredPurchase` + - `IGooglePlayStoreExtensions.SetDeferredProrationUpgradeDowngradeSubscriptionListener` to `IGooglePlayConfiguration.SetDeferredProrationUpgradeDowngradeSubscriptionListener` - `IGooglePlayStoreExtensions.SetDeferredPurchaseListener` to `IGooglePlayConfiguration.SetDeferredPurchaseListener` - - `IGooglePlayStoreExtensions.SetObfuscatedAccountId` to `IGooglePlayConfiguration.SetObfuscatedAccountId` + - `IGooglePlayStoreExtensions.SetObfuscatedAccountId` to `IGooglePlayConfiguration.SetObfuscatedAccountId` - `IGooglePlayStoreExtensions.SetObfuscatedProfileId` to `IGooglePlayConfiguration.SetObfuscatedProfileId` - Apple - Change the order of execution of the post-process build script, which adds the `StoreKitFramework` such that other post-process build scripts can run after it. -- Changed the __Target Android__ Menu app store selection feature to display a window under `Window > Unity IAP > Switch Store...`. To set the app store for the next build, first use __Build Settings__ to activate the Android build target. +- Changed the __Target Android__ Menu app store selection feature to display a window under `Window > Unity IAP > Switch Store...`. To set the app store for the next build, first use __Build Settings__ to activate the Android build target. - For the future Unity 2022 - Moved Unity IAP menu items from `Window > Unity IAP > ...` to `Services > In-App Purchasing > ...` - Updated and added new functionnality to the `Services > In-App Purchasing` window in the `Project Settings`. The `Current Targeted Store` selector and `Receipt Obfuscator` settings are now accessible from this window. @@ -133,7 +294,7 @@ an `OnPurchaseFailed` would be called with the purchase failure reason `UserCanc - `UnityPurchasingEditor.TargetAndroidStore(AndroidStore)`. Use `TargetAndroidStore(AppStore)` instead. - `WinRT` class. Use `WindowsStore` instead. - `WindowsPhone8` class. Use `WindowsStore` instead. - + ## [3.2.3] - 2021-07-08 ### Fixed - GooglePlay - Fix `DuplicateTransaction` errors seen during purchase, after a purchase had previously been Acknowledged with Google. @@ -150,7 +311,7 @@ an `OnPurchaseFailed` would be called with the purchase failure reason `UserCanc ## [3.2.1] - 2021-05-18 ### Changed -- Manual and API documentation updated. +- Manual and API documentation updated. ## [3.2.0] - 2021-05-17 ### Added @@ -198,7 +359,7 @@ an `OnPurchaseFailed` would be called with the purchase failure reason `UserCanc ### Added - GooglePlay - populate `Product.receipt` for `Action` parameter returned by `IGooglePlayStoreExtensions.SetDeferredPurchaseListener` callback -### Changed +### Changed - WinRT - This feature is now shipped as C# code under assembly definitions instead of .dll files. - Security - This feature is now shipped as C# code under assembly definitions instead of .dll files. - Receipt Validation Obfuscator - The Tangle File Obfuscate function is now Editor-only and no longer part of the Runtime Security module. @@ -214,7 +375,7 @@ an `OnPurchaseFailed` would be called with the purchase failure reason `UserCanc ## [3.0.0-pre.5] - 2021-01-12 ### Added -- Apple - Support for [auto-renewable subscription Offer Codes](https://developer.apple.com/documentation/storekit/in-app_purchase/subscriptions_and_offers/implementing_offer_codes_in_your_app) on iOS and iPadOS 14 and later via `IAppleExtensions.PresentOfferRedemptionSheet()`. E.g. +- Apple - Support for [auto-renewable subscription Offer Codes](https://developer.apple.com/documentation/storekit/in-app_purchase/subscriptions_and_offers/implementing_offer_codes_in_your_app) on iOS and iPadOS 14 and later via `IAppleExtensions.PresentOfferRedemptionSheet()`. E.g. ```csharp public void ShowSubscriptionOfferRedemption(IExtensionProvider extensions) @@ -225,7 +386,7 @@ public void ShowSubscriptionOfferRedemption(IExtensionProvider extensions) ``` ### Fixed - - Security and WinRT stub dlls and references to Analytics no longer break builds unsupported platforms like PS4, XboxOne, Switch and Lumin. These platforms are still unsupported but will no longer raise errors on build. + - Security and WinRT stub dlls and references to Analytics no longer break builds unsupported platforms like PS4, XboxOne, Switch and Lumin. These platforms are still unsupported but will no longer raise errors on build. ### Removed - Support for Facebook in-app purchasing is no longer provided. All classes and implementations have been removed. @@ -272,7 +433,7 @@ Fix migration tooling's obfuscator file destination path to target Scripts inste - Added editor and playmode testing. ## [2.0.3] - 2018-06-14 -- Fixed issue related to 2.0.2 that caused new projects to not compile in the editor. +- Fixed issue related to 2.0.2 that caused new projects to not compile in the editor. - Engine dll is enabled for editor by default. - Removed meta data that disabled engine dll for windows store. diff --git a/Documentation~/AmazonTesting.md b/Documentation~/AmazonTesting.md index 28e7495..cf98278 100644 --- a/Documentation~/AmazonTesting.md +++ b/Documentation~/AmazonTesting.md @@ -9,18 +9,18 @@ var builder = ConfigurationBuilder.Instance( StandardPurchasingModule.Instance()); // Define your products. builder.AddProduct("someConsumable", ProductType.Consumable); -// Write a product description to the SD card +// Write a product description to the SD card // in the appropriate location. builder.Configure() - .WriteSandboxJSON(builder.products); + .WriteSandboxJSON(builder.products); ```` When using this method to write product descriptions to the SD card, declare the Android permission to write to external storage in the test app’s manifest: ```` - + ```` -Remove this extra permission before publishing, if appropriate. +Remove this extra permission before publishing, if appropriate. -Amazon Sandbox is now set up for local testing. For more information, please see Amazon's [App Tester documentation](https://developer.amazon.com/public/apis/earn/in-app-purchasing/docs-v2/installing-and-configuring-app-tester). \ No newline at end of file +Amazon Sandbox is now set up for local testing. For more information, please see Amazon's [App Tester documentation](https://developer.amazon.com/public/apis/earn/in-app-purchasing/docs-v2/installing-and-configuring-app-tester). diff --git a/Documentation~/AppleReceipt.md b/Documentation~/AppleReceipt.md index 9a40604..418ecd6 100644 --- a/Documentation~/AppleReceipt.md +++ b/Documentation~/AppleReceipt.md @@ -15,4 +15,4 @@ Payload varies depending upon the device's iOS version. Mac App Store ------------- -Payload is a base 64 encoded [App Receipt](https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#/apple_ref/doc/uid/TP40010573-CH106-SW1). \ No newline at end of file +Payload is a base 64 encoded [App Receipt](https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#/apple_ref/doc/uid/TP40010573-CH106-SW1). diff --git a/Documentation~/AppleTesting.md b/Documentation~/AppleTesting.md index ab6ff61..34249d3 100644 --- a/Documentation~/AppleTesting.md +++ b/Documentation~/AppleTesting.md @@ -31,5 +31,3 @@ To sign the bundle, you may first need to remove the Contents.meta file if it ex In order to install the package correctly you must delete the unpackaged .app file before running the newly created package. You must then launch your App from the Applications folder. The first time you do so, you will be prompted to enter your iTunes account details, for which you should enter your App Store Connect test user account login. You will then be able to make test purchases against the sandbox environment. - - diff --git a/Documentation~/BackendReceiptValidation.md b/Documentation~/BackendReceiptValidation.md index ebe798f..774eeb0 100644 --- a/Documentation~/BackendReceiptValidation.md +++ b/Documentation~/BackendReceiptValidation.md @@ -2,4 +2,4 @@ Backend receipt validation helps you prevent users from accessing content they have not purchased. -For server-side content, where content is downloaded once purchased, the validation should take place on the server before the content is released. Unity does not offer support for server-side validation; however, third-party solutions are available, such as Nobuyori Takahashi’s [IAP project](https://github.com/voltrue2/in-app-purchase). \ No newline at end of file +For server-side content, where content is downloaded once purchased, the validation should take place on the server before the content is released. Unity does not offer support for server-side validation; however, third-party solutions are available, such as Nobuyori Takahashi’s [IAP project](https://github.com/voltrue2/in-app-purchase). diff --git a/Documentation~/CodelessIAPButton.md b/Documentation~/CodelessIAPButton.md new file mode 100644 index 0000000..10e0d89 --- /dev/null +++ b/Documentation~/CodelessIAPButton.md @@ -0,0 +1,63 @@ +# IAP Button + +IAP Button is a way to purchase or restore products without writing code. + +## Adding IAP Button to the Scene + +To add an __IAP Button__ to your Scene, in the Unity Editor, select __Window > Unity IAP > Create IAP Button__. + +![Creating a Codeless **IAP Button** in the Unity Editor](images/CreateButton.png) + +## Handling OnProductFetched +This event will be triggered when IAP retrieves product information from the app stores. It is a good idea to update all text related to the product in the UI with this event such as title, description and price. + +**On Product Fetched script example**: +``` +public class IAPButtonView : MonoBehaviour +{ + [SerializeField] + Text title; + [SerializeField] + TMP_Text price; + + public void OnProductFetched(Product product) + { + if (title != null) + { + title.text = product.metadata.localizedTitle; + } + + if (price != null) + { + price.text = product.metadata.localizedPriceString; + } + } +} +``` + +A script like above can be added to the IAPButton to link different views with this event. + +## Restore Button +Some app stores, including iTunes, require apps to have a __Restore__ button. Codeless IAP provides an easy way to implement a restore button in your app. + +To add a __Restore__ button: + +1. Add an __IAP Button__ to your Scene (**Services** > **In-App Purchasing** > **Create IAP Button**). +2. With your __IAP Button__ selected, locate its **IAP Button (Script)** component in the Inspector, then select **Restore** from the **Button Type** drop-down menu (most of the component's other fields will disappear from the Inspector view). + ![Modifying an IAP Button to restore purchases](images/CodelessIAPButtonRestoreButton.png) +3. (Optional) You can add a script by clicking the plus (**+**) button to add a script to the **On Transactions Restored (Boolean, String)**. +4. (Optional) Drag the GameObject with the restore transactions script onto the event field in the component’s Inspector, then select your function from the dropdown menu. + +**On Transactions Restored script example**: + +``` +public void OnTransactionsRestored(bool success, string? error) +{ + Debug.Log($"TransactionsRestored: {success} {error}"); +} +``` + +When a user selects this button at run time, the button calls the purchase restoration API for the current store. This functionality works on the iOS App Store, the Mac App Store and the Windows Store. You may want to hide the __Restore__ button on other platforms. + +Unity IAP will always invoke the __On Transactions Restored (Boolean, String)__ function on the __Restore IAP Button__ with the result and the associated error message if the restore fails. +If the restore succeeds, Unity IAP invokes the __On Purchase Complete (Product)__ function on the __IAP Button__ associated with that Product. diff --git a/Documentation~/DefiningProductsCoded.md b/Documentation~/DefiningProductsCoded.md index 7f7a892..437de61 100644 --- a/Documentation~/DefiningProductsCoded.md +++ b/Documentation~/DefiningProductsCoded.md @@ -1,6 +1,6 @@ # Defining Products in scripts -You can declare your Product list programmatically using the [Purchasing Configuration Builder](xref:UnityEngine.Purchasing.ConfigurationBuilder). +You can declare your Product list programmatically using the [Purchasing Configuration Builder](xref:UnityEngine.Purchasing.ConfigurationBuilder). You must provide a unique cross-store __Product ID__ and __Product Type__ for each Product: ```` diff --git a/Documentation~/DefiningProductsOverview.md b/Documentation~/DefiningProductsOverview.md index 6a26482..bb608f0 100644 --- a/Documentation~/DefiningProductsOverview.md +++ b/Documentation~/DefiningProductsOverview.md @@ -1,7 +1,7 @@ # Defining products ## Product ID -Enter a cross-platform unique identifier to serve as the Product’s default ID when communicating with an app store. +Enter a cross-platform unique identifier to serve as the Product’s default ID when communicating with an app store. **Important**: The ID may only contain lowercase letters, numbers, underscores, or periods. @@ -34,12 +34,11 @@ Use this section to add local, fixed definitions for the content you pay out to | __Payout Type__ | Enum | Defines the category of content the purchaser receives. There are four possible Types. | * Currency
* Item
* Resource
* Other| | __Payout Subtype__ | String | Provides a level of granularity to the content category. |* “Gold” and “Silver” subtypes of a __Currency__ type
* “Potion” and “Boost” subtypes of an __Item__ type | | __Quantity__ | Int | Specifies the number of items, currency, and so on, that the purchaser receives in the payout. | * 1
* >25
* 100| -| __Data__ | | Use this field any way you like as a property to reference in code. | * Flag for a UI element
* Item rarity | +| __Data__ | | Use this field any way you like as a property to reference in code. | * Flag for a UI element
* Item rarity | -**Note**: You can add multiple Payouts to a single Product. +**Note**: You can add multiple Payouts to a single Product. For more information on the PayoutDefinition class, see the [Scripting Reference](xref:UnityEngine.Purchasing.PayoutDefinition). You can always add Payout information to a Product in a script using this class. For example: ### Store ID Overrides By default, Unity IAP assumes that your Product has the same identifier (specified in the **ID** field, above) across all app stores. Unity recommends doing this where possible. However, there are occasions when this is not possible, such as when publishing to both iOS and Mac stores, which prohibit developers from using the same product ID across both. - diff --git a/Documentation~/GettingStarted.md b/Documentation~/GettingStarted.md index cff0519..58a889b 100644 --- a/Documentation~/GettingStarted.md +++ b/Documentation~/GettingStarted.md @@ -15,4 +15,4 @@ 7. Click the toggle next to **In-App Purchasing Settings** to **ON**. -This will automatically install the IAP package from the Package Manager, providing you with new features and menu items to help you manage IAP. \ No newline at end of file +This will automatically install the IAP package from the Package Manager, providing you with new features and menu items to help you manage IAP. diff --git a/Documentation~/GooglePublicKey.md b/Documentation~/GooglePublicKey.md index 233c9ec..5784546 100644 --- a/Documentation~/GooglePublicKey.md +++ b/Documentation~/GooglePublicKey.md @@ -14,4 +14,4 @@ It is possible to set the Google Public Key in two different places either in th 2. Open the left menu and select `Settings` then `Project Settings` under `Project` ![GooglePublicKeyDashboardSetting](images/IAPGooglePublicKeyDashboardSetting.png) and select the project 3. In the section `In-app purchase (IAP) settings` edit the field `Google License Key` -![GooglePublicKeyDashboard](images/IAPGooglePublicKeyDashboard.png) \ No newline at end of file +![GooglePublicKeyDashboard](images/IAPGooglePublicKeyDashboard.png) diff --git a/Documentation~/GoogleReceipt.md b/Documentation~/GoogleReceipt.md index fa7a680..abca7b7 100644 --- a/Documentation~/GoogleReceipt.md +++ b/Documentation~/GoogleReceipt.md @@ -10,4 +10,4 @@ Payload is a JSON hash with the following keys and values: |Key|Value| |:---|:---| |__json__|A JSON encoded string provided by Google; [`INAPP_PURCHASE_DATA`](http://developer.android.com/google/play/billing/billing_reference.html)| -|__signature__|A signature for the json parameter, as provided by Google; [`INAPP_DATA_SIGNATURE`](http://developer.android.com/google/play/billing/billing_reference.html)| \ No newline at end of file +|__signature__|A signature for the json parameter, as provided by Google; [`INAPP_DATA_SIGNATURE`](http://developer.android.com/google/play/billing/billing_reference.html)| diff --git a/Documentation~/HowToTest.md b/Documentation~/HowToTest.md index 8472aad..e169ecd 100644 --- a/Documentation~/HowToTest.md +++ b/Documentation~/HowToTest.md @@ -16,4 +16,4 @@ This option display no dialog. This option will show a simple dialog is shown when Purchasing. ### 3. DeveloperUser -This option will show a dialog giving options for failure reason code selection when Initializing/Retrieving Products and when Purchasing. \ No newline at end of file +This option will show a dialog giving options for failure reason code selection when Initializing/Retrieving Products and when Purchasing. diff --git a/Documentation~/IAPButton.md b/Documentation~/IAPButton.md index 350808b..5ef4300 100644 --- a/Documentation~/IAPButton.md +++ b/Documentation~/IAPButton.md @@ -1,10 +1,12 @@ -# IAP Button +# IAP Button (Legacy) + +IAP Button (Legacy) is obsolete. Please use [IAP Button](CodelessIAPButton.md). This new button allows for more UI customization and new events such as `OnProductFetched(Product)`. IAP Button is a way to purchase or restore products without writing code. ## Adding IAP Button to the Scene -To add an __IAP Button__ to your Scene, in the Unity Editor, select __Window > Unity IAP > Create IAP Button__. +To add an __IAP Button__ to your Scene, in the Unity Editor, select __Window > Unity IAP > Create IAP Button (Legacy)__. ![Creating a Codeless **IAP Button** in the Unity Editor](images/CreateButton.png) @@ -16,7 +18,19 @@ To add a __Restore__ button: 1. Add an __IAP Button__ to your Scene (**Services** > **In-App Purchasing** > **Create IAP Button**). 2. With your __IAP Button__ selected, locate its **IAP Button (Script)** component in the Inspector, then select **Restore** from the **Button Type** drop-down menu (most of the component's other fields will disappear from the Inspector view). ![Modifying an IAP Button to restore purchases](images/RestoreButton.png) +3. (Optional) You can add a script by clicking the plus (**+**) button to add a script to the **On Transactions Restored (Boolean, String)**. +4. (Optional) Drag the GameObject with the restore transactions script onto the event field in the component’s Inspector, then select your function from the dropdown menu. + +**On Transactions Restored script example**: + +``` +public void OnTransactionsRestored(bool success, string? error) +{ + Debug.Log($"TransactionsRestored: {success} {error}"); +} +``` When a user selects this button at run time, the button calls the purchase restoration API for the current store. This functionality works on the iOS App Store, the Mac App Store and the Windows Store. You may want to hide the __Restore__ button on other platforms. +Unity IAP will always invoke the __On Transactions Restored (Boolean, String)__ function on the __Restore IAP Button__ with the result and the associated error message if the restore fails. If the restore succeeds, Unity IAP invokes the __On Purchase Complete (Product)__ function on the __IAP Button__ associated with that Product. diff --git a/Documentation~/IAPListener.md b/Documentation~/IAPListener.md index 74550b4..e7095d4 100644 --- a/Documentation~/IAPListener.md +++ b/Documentation~/IAPListener.md @@ -28,7 +28,7 @@ When your catalog contains at least one Product, you can define __IAP Button__ b public void GrantCredits (int credits){ userCredits = userCredits + credits; Debug.Log(“You received “ + credits “ Credits!”); -} +} ``` Run your game to test the __IAP Button__. diff --git a/Documentation~/InitializationOverview.md b/Documentation~/InitializationOverview.md new file mode 100644 index 0000000..a362c42 --- /dev/null +++ b/Documentation~/InitializationOverview.md @@ -0,0 +1,6 @@ +# Initialization + +In order to use the **Unity In-App Purchasing** package, **Unity Gaming Services** needs to be initialized first followed by the initialization of **Unity In-App Purchasing**. + +The following diagram visually describes steps to initialize Unity In-App Purchasing. +![Initialization flow diagram](images/UGSInitializationFlowDiagram.png) diff --git a/Documentation~/Overview.md b/Documentation~/Overview.md index 0dcb19a..80f2ba9 100644 --- a/Documentation~/Overview.md +++ b/Documentation~/Overview.md @@ -58,4 +58,4 @@ More information can be found in the `Stores` section of this manual #### Unity Learn IAP classes -[Refer to the Unity Learn IAP classes](https://learn.unity.com/tutorial/unity-iap) for more guidance. +[Refer to the Unity Learn IAP classes](https://learn.unity.com/tutorial/unity-iap) for more guidance. diff --git a/Documentation~/StoresSupported.md b/Documentation~/StoresSupported.md index c8da56a..3587ad9 100644 --- a/Documentation~/StoresSupported.md +++ b/Documentation~/StoresSupported.md @@ -5,9 +5,9 @@ The following is the full list of stores supported by the In-App Purchasing pack |Store Name|Platform|Version|Website| |---|---|---|---| -|Google Billing|Android|3.0.3|[Google Release Notes](https://developer.android.com/google/play/billing/release-notes)| +|Google Billing|Android| 5.1.0|[Google Release Notes](https://developer.android.com/google/play/billing/release-notes)| |Amazon Appstore|Android|2.0.76|[Amazon SDK](https://developer.amazon.com/docs/in-app-purchasing/iap-get-started.html#download-the-iap-sdk)| |Samsung|Android|Removed use [UDP](https://unity.com/products/unity-distribution-portal) instead| [UDP](https://unity.com/products/unity-distribution-portal)| |Unity Distribution Portal|Android|2.0.0 and higher|[UDP](https://unity.com/products/unity-distribution-portal)| |App Store|MacOS / iOS / tvOS|Store Kit v1|[Apple Store Kit](https://developer.apple.com/documentation/storekit)| -|Microsoft Store|Windows||[Microsoft SDK](https://docs.microsoft.com/en-us/windows/uwp/monetize/in-app-purchases-and-trials)| \ No newline at end of file +|Microsoft Store|Windows||[Microsoft SDK](https://docs.microsoft.com/en-us/windows/uwp/monetize/in-app-purchases-and-trials)| diff --git a/Documentation~/TableOfContents.md b/Documentation~/TableOfContents.md index 1ad4f90..14f2ad8 100644 --- a/Documentation~/TableOfContents.md +++ b/Documentation~/TableOfContents.md @@ -8,11 +8,15 @@ * [Overview](DefiningProductsOverview.md) * [Coded](DefiningProductsCoded.md) * [IAP Catalog](UnityIAPDefiningProducts.md) - * [Initialize IAP](UnityIAPInitialization.md) + * Initialization + * [Overview](InitializationOverview.md) + * [Initialize Unity Gaming Services](UnityIAPInitializeUnityGamingServices.md) + * [Initialize IAP](UnityIAPInitialization.md) * [Fetching Additional Products](UnityIAPFetchingProductsIncrementally.md) * Creating a Purchasing Button * [Browsing Product Metadata](UnityIAPBrowsingMetadata.md) - * [IAP Button](IAPButton.md) + * [IAP Button](CodelessIAPButton.md) + * [IAP Button (Legacy)](IAPButton.md) * [Coded](UnityIAPInitiatingPurchases.md) * The Purchasing Flow * Processing Purchases @@ -44,6 +48,7 @@ * [Extensions and Configuration](UnityIAPiOSMAS.md) * [Purchase Receipt](AppleReceipt.md) * [Testing](AppleTesting.md) + * [Family Sharing](UnityIAPAppleFamilySharing.md) * Microsoft Store (UWP) * [How to Set Up](UnityIAPWindowsConfiguration.md) * [Purchase Receipt](MicrosoftReceipt.md) diff --git a/Documentation~/UnityIAPAmazonConfiguration.md b/Documentation~/UnityIAPAmazonConfiguration.md index 1323e5f..7f34059 100644 --- a/Documentation~/UnityIAPAmazonConfiguration.md +++ b/Documentation~/UnityIAPAmazonConfiguration.md @@ -2,7 +2,7 @@ ## Introduction -This guide describes the process of setting up the Amazon Appstore for use with the Unity in-app purchasing (IAP) system. This includes establishing the digital records and relationships that are required to interact with the Unity IAP API, setting up an Amazon developer account, and testing and publishing a Unity IAP application. +This guide describes the process of setting up the Amazon Appstore for use with the Unity in-app purchasing (IAP) system. This includes establishing the digital records and relationships that are required to interact with the Unity IAP API, setting up an Amazon developer account, and testing and publishing a Unity IAP application. As with other platforms, the Amazon store allows for the purchase of virtual goods and managed products. These digital products are identified using a string identifier and an additional type to define durability, with choices including subscription (capable of being subscribed to), consumable (capable of being rebought), and non-consumable (capable of being bought once). @@ -19,15 +19,15 @@ As with other platforms, the Amazon store allows for the purchase of virtual goo 2. For FireOS devices, the Amazon Appstore should come pre-installed.

**Note**: Though you may freely target FireOS devices, FireOS is not a Unity-supported platform.

3. Once you have installed the Amazon Appstore, install the [Amazon App Tester](http://www.amazon.com/Amazon-App-Tester/dp/B00BN3YZM2/). - ![](images/AmazonConfiguration-AmazonAppTester.png) + ![](images/AmazonConfiguration-AmazonAppTester.png) 1. Set up the Android SDK 1. To install and watch the Android debug log, ensure you have the [Android SDK](https://developer.android.com/studio/install.html) installed. Download the relevant command line tools package from the Android SDK install page and extract them to your computer. 1. Confirm that the SDK recognizes the attached Android device through the command-line adb tool. For example: - + ```` |[11:07:01] user@laptop:/Applications | $ adb devices List of devices attached -00DA0807526300W5 device +00DA0807526300W5 device ```` ### Unity app setup @@ -48,6 +48,3 @@ It's not necessary to download Amazon's native IAP plug-in when preparing to use 1. Set up your catalog. Using the product descriptions you prepared earlier, add the items to the Amazon catalog using the Amazon Developer Portal. Navigate to your app's page, and find the __In-App Items section__. Use the __Add a Consumable__, __Add an Entitlement__, or __Add a Subscription__ buttons to set up your catalog. ![](images/AmazonConfiguration-SetUpCatalog.png) - - - diff --git a/Documentation~/UnityIAPAmazonExtendedFunctionality.md b/Documentation~/UnityIAPAmazonExtendedFunctionality.md index 989eab9..005026d 100644 --- a/Documentation~/UnityIAPAmazonExtendedFunctionality.md +++ b/Documentation~/UnityIAPAmazonExtendedFunctionality.md @@ -10,9 +10,8 @@ To fetch the current Amazon User ID for other Amazon services, use the `IAmazonE public void OnInitialized (IStoreController controller, IExtensionProvider extensions) { - string amazonUserId = + string amazonUserId = extensions.GetExtension().amazonUserId; // ... } ```` - diff --git a/Documentation~/UnityIAPAppleConfiguration.md b/Documentation~/UnityIAPAppleConfiguration.md index 3913699..463def6 100644 --- a/Documentation~/UnityIAPAppleConfiguration.md +++ b/Documentation~/UnityIAPAppleConfiguration.md @@ -4,7 +4,7 @@ This guide describes the process of establishing the digital records and relationships necessary for a Unity game to interact with an In-App Purchase Store. -In-App Purchase (IAP) is the process of transacting money for digital goods. A platform's Store allows purchase of Products, representing digital goods. These Products have an Identifier, typically of string datatype. Products have Types to represent their durability: _subscription_, _consumable_ (capable of being rebought), and _non-consumable_ (capable of being bought only once) are the most common. +In-App Purchase (IAP) is the process of transacting money for digital goods. A platform's Store allows purchase of Products, representing digital goods. These Products have an Identifier, typically of string datatype. Products have Types to represent their durability: _subscription_, _consumable_ (capable of being rebought), and _non-consumable_ (capable of being bought only once) are the most common. ## Apple App Store @@ -18,40 +18,40 @@ In-App Purchase (IAP) is the process of transacting money for digital goods. A p ### Register the Application -1. In the [Apple Developer Center](https://developer.apple.com/account), navigate to the appropriate Identifiers section. +1. In the [Apple Developer Center](https://developer.apple.com/account), navigate to the appropriate Identifiers section. + +2. Add a new App ID to create a fundamental application entity with Apple. -2. Add a new App ID to create a fundamental application entity with Apple. - **NOTE:** Use an Explicit App ID. Wildcard App IDs (com.example.*) cannot be used for applications that use In-App Purchases. **NOTE:** The App ID is available to use in App Store Connect after you create it in the Developer Center. - + ![](images/IAPAppleImage1.png) -3. Navigate to [App Store Connect](https://itunesconnect.apple.com) and create an App, to establish a Store relationship with your game. - - ![](images/IAPAppleImage2.png) +3. Navigate to [App Store Connect](https://itunesconnect.apple.com) and create an App, to establish a Store relationship with your game. + + ![](images/IAPAppleImage2.png) -4. Use the newly created App ID for the app's Bundle ID. +4. Use the newly created App ID for the app's Bundle ID. ![](images/IAPAppleImage3.png) ### Add In-App Purchases -1. Choose __Features__ and add a new In-App Purchase with the plus ("+") button. +1. Choose __Features__ and add a new In-App Purchase with the plus ("+") button. ![](images/IAPAppleImage4.png) -2. Choose a [Product Type](DefiningProductsOverview.md#Product-Type). +2. Choose a [Product Type](DefiningProductsOverview.md#Product-Type). ![](images/IAPAppleImage5.png) 3. Specify the Product Identifier, and complete other fields as requested. - **NOTE:** The "Product ID" here is the same identifier used in the game source code, added to the [Unity IAP ConfigurationBuilder](xref:UnityEngine.Purchasing.ConfigurationBuilder) instance via __AddProduct()__ or __AddProducts()__. - - **NOTE:** When targeting multiple Apple device groups (for example, shipping on both iOS and Mac) Apple requires usage of different, unique product identifiers for each distinct device group. Use [Unity IAP's Purchasing.IDs](xref:UnityEngine.Purchasing.IDs) class and define a one-to-many mapping Product IDs to the store-specific identifiers, and pass that mapping in when initializing IAP. + **NOTE:** The "Product ID" here is the same identifier used in the game source code, added to the [Unity IAP ConfigurationBuilder](xref:UnityEngine.Purchasing.ConfigurationBuilder) instance via __AddProduct()__ or __AddProducts()__. + + **NOTE:** When targeting multiple Apple device groups (for example, shipping on both iOS and Mac) Apple requires usage of different, unique product identifiers for each distinct device group. Use [Unity IAP's Purchasing.IDs](xref:UnityEngine.Purchasing.IDs) class and define a one-to-many mapping Product IDs to the store-specific identifiers, and pass that mapping in when initializing IAP. ![](images/IAPAppleImage6.png) @@ -61,13 +61,13 @@ In-App Purchase (IAP) is the process of transacting money for digital goods. A p ### Test IAP -1. Create __Sandbox Testers__ using App Store Connect for use on your test device's iTunes Account. To do this, navigate to __App Store Connect > Users and Roles__, and choose the plus ("+") button. You must review [Apple's Sandbox Tester documentation](https://help.apple.com/app-store-connect/#/dev8b997bee1) as there are several additional important usage notes, and you must use a real email address to create Testers. - - **TIP:** (*) To simplify managing the email address, use an email service capable of sub-addressing (emailaccount+subaddress@example.com) such as Gmail, iCloud, and Outlook.com. This allows one email account to receive email for multiple sub-addresses. +1. Create __Sandbox Testers__ using App Store Connect for use on your test device's iTunes Account. To do this, navigate to __App Store Connect > Users and Roles__, and choose the plus ("+") button. You must review [Apple's Sandbox Tester documentation](https://help.apple.com/app-store-connect/#/dev8b997bee1) as there are several additional important usage notes, and you must use a real email address to create Testers. + + **TIP:** (*) To simplify managing the email address, use an email service capable of sub-addressing (emailaccount+subaddress@example.com) such as Gmail, iCloud, and Outlook.com. This allows one email account to receive email for multiple sub-addresses. ![](images/IAPAppleImage8.png) -2. Walk through the user creation wizard. +2. Walk through the user creation wizard. ![](images/IAPAppleImage9.png) @@ -85,12 +85,12 @@ In-App Purchase (IAP) is the process of transacting money for digital goods. A p ![](images/IAPAppleImage11.png) -2. Build and run the game on your iOS device. `UnityPurchasing.Initialize()` succeeds if everything has been correctly configured. See [Unity Purchasing Initialization](xref:UnityEngine.Purchasing.UnityPurchasing) +2. Build and run the game on your iOS device. `UnityPurchasing.Initialize()` succeeds if everything has been correctly configured. See [Unity Purchasing Initialization](xref:UnityEngine.Purchasing.UnityPurchasing) -3. Test the IAP by making a purchase in the game on the device. A modified purchase dialog displays, explaining that this purchase is being performed in the Sandbox Environment. Use the Sandbox User Tester password when prompted for purchase. +3. Test the IAP by making a purchase in the game on the device. A modified purchase dialog displays, explaining that this purchase is being performed in the Sandbox Environment. Use the Sandbox User Tester password when prompted for purchase. WARNING: If the indicator is not present, then an account is charged real money for the product. - + ![](images/IAPAppleImage12.png) #### For Mac @@ -102,11 +102,11 @@ In-App Purchase (IAP) is the process of transacting money for digital goods. A p 3. Sign, package, and install your application. Run the following commands from an OSX terminal, filling in "your.app" and "your.pkg" appropriately. **TIP:** To sign the bundle, you may first need to remove the Contents.meta file if it exists: `your.app/Contents/Plugins/unitypurchasing.bundle/Contents.meta` - + 1. `codesign -f --deep -s "3rd Party Mac Developer Application: " your.app/Contents/Plugins/unitypurchasing.bundle` - + 1. `codesign -f --deep -s "3rd Party Mac Developer Application: " your.app` - + 1. `productbuild --component your.app /Applications --sign "3rd Party Mac Developer Installer: " your.pkg` 4. To install the package correctly, delete the unpackaged .app file before running the newly created package and installing it. @@ -114,6 +114,4 @@ In-App Purchase (IAP) is the process of transacting money for digital goods. A p 5. Launch the app from the _Applications_ folder. The first time you do so, you are prompted to enter your iTunes account details, for which you can then make test purchases against the sandbox environment. -See pages on [iOS and Mac Extended Functionality](UnityIAPiOSMAS.md) and [Delivering applications to the Apple Mac Store](https://docs.unity3d.com/Manual/HOWTO-PortToAppleMacStore.html) for additional details on Apple App Store testing and signing. - - +See pages on [iOS and Mac Extended Functionality](UnityIAPiOSMAS.md) and [Delivering applications to the Apple Mac Store](https://docs.unity3d.com/Manual/HOWTO-PortToAppleMacStore.html) for additional details on Apple App Store testing and signing. diff --git a/Documentation~/UnityIAPAppleFamilySharing.md b/Documentation~/UnityIAPAppleFamilySharing.md new file mode 100644 index 0000000..e8211a1 --- /dev/null +++ b/Documentation~/UnityIAPAppleFamilySharing.md @@ -0,0 +1,40 @@ +# [Apple Family Sharing](https://developer.apple.com/app-store/subscriptions/#family-sharing) + +## Introduction + +Apple allows auto-renewable subscriptions and non-consumable in-app purchases to be shared within a family. +In order to use this feature, Family Sharing must be enabled on a per purchasable basis. See [Turn on Family Sharing for in-app purchases](https://help.apple.com/app-store-connect/#/dev45b03fab9). + +### Is Family Shareable + +The family shareable status of a product is available through the `isFamilyShareable` field found in the Apple product metadata. +The metadata can be obtained from `ProductMetadata.GetAppleProductMetadata()` via `IStoreController.products`. +```` + bool IsProductFamilyShareable(Product product) + { + var appleProductMetadata = product.metadata.GetAppleProductMetadata(); + return appleProductMetadata?.isFamilyShareable ?? false; + } +```` + +### Revoke Entitlement + +In order to be handle revoked entitlements, you can specify a listener through the `IAppleConfiguration.SetEntitlementsRevokedListener(Action>`. +This will be called each time products have been revoked with the list of revoked products. +```` + void InitializePurchasing() + { + var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance()); + builder.Configure().SetEntitlementsRevokedListener(EntitlementsRevokeListener); + + UnityPurchasing.Initialize(this, builder); + } + + void EntitlementsRevokeListener(List revokedProducts) + { + foreach (var revokedProduct in revokedProducts) + { + Debug.Log($"Revoked product: {revokedProduct.definition.id}"); + } + } +```` diff --git a/Documentation~/UnityIAPBrowsingMetadata.md b/Documentation~/UnityIAPBrowsingMetadata.md index 33bb225..23e50c8 100644 --- a/Documentation~/UnityIAPBrowsingMetadata.md +++ b/Documentation~/UnityIAPBrowsingMetadata.md @@ -9,4 +9,3 @@ foreach (var product in controller.products.all) { Debug.Log (product.metadata.localizedPriceString); } ```` - diff --git a/Documentation~/UnityIAPDefiningProducts.md b/Documentation~/UnityIAPDefiningProducts.md index c7a6399..e70d23c 100644 --- a/Documentation~/UnityIAPDefiningProducts.md +++ b/Documentation~/UnityIAPDefiningProducts.md @@ -1,9 +1,9 @@ # IAP Catalog To open the __IAP Catalog__ GUI one of two ways: - + * Select __Services > In-App Purchasing > IAP Catalog__. -* Or, with your __IAP Button__ selected, locate its __IAP Button (Script)__ component in the Inspector, then click __IAP Catalog…__. +* Or, with your __IAP Button__ selected, locate its __IAP Button (Script)__ component in the Inspector, then click __IAP Catalog…__. ![Accessing the **IAP Catalog** GUI through an **IAP Button** script component](images/OpenCatalogGUI.png) @@ -16,7 +16,7 @@ Next, use the GUI to define the following attributes for each Product in your ca ![Populating Product information in the **IAP Catalog** GUI](images/IAPCatalogGUI.png) -**Note:** +**Note:** - The __IAP Catalog__ GUI provides additional tools for configuring your Products. Before [exporting a catalog](#Exporting-to-an-app-store) for upload to its respective store, you must populate description and pricing information as well. - The __IAP Catalog__ acts as a Product catalog dictionary, not as an inventory manager. You must still implement the code that handles conveyance of the purchased content. @@ -31,7 +31,7 @@ This sections defines the [descriptions of a product](DefiningProductsOverview.m Add __Translations__ for the __Title__ and __Description__ fields by clicking the plus (__+__) icon and selecting an additional locale. You can add as many translations as you like. ### Payouts -This sections defines the [payout of a product](DefiningProductsOverview.md#Payouts). +This sections defines the [payout of a product](DefiningProductsOverview.md#Payouts). ![Populating **Payouts** fields for Products in the **IAP Catalog** GUI](images/Payouts.png) @@ -48,7 +48,7 @@ Provide either a Product price, or an ID for a [Pricing Template](https://suppor ### Apple Configuration (required for Apple export) Select a **Pricing Tier** from the dropdown menu. Unity supports predefined Apple price points, but not arbitrary values. -__Select a screenshot__ to upload. +__Select a screenshot__ to upload. For information on screenshot specs, see Apple’s publisher support documentation. diff --git a/Documentation~/UnityIAPFetchingProductsIncrementally.md b/Documentation~/UnityIAPFetchingProductsIncrementally.md index 598a13a..232eb67 100644 --- a/Documentation~/UnityIAPFetchingProductsIncrementally.md +++ b/Documentation~/UnityIAPFetchingProductsIncrementally.md @@ -12,26 +12,26 @@ An alternative is to initialise Unity IAP with an initial set of products, then /// public void OnInitialized(IStoreController controller, IExtensionProvider extensions) { - var additional = new HashSet() { - new ProductDefinition("coins.500", ProductType.Consumable), - new ProductDefinition("armour", ProductType.NonConsumable) - }; - - Action onSuccess = () => { - Debug.Log("Fetched successfully!"); - // The additional products are added to the set of - // previously retrieved products and are browseable - // and purchasable. - foreach (var product in controller.products.all) { - Debug.Log(product.definition.id); - } - }; - - Action onFailure = (InitializationFailureReason i) => { - Debug.Log("Fetching failed for the specified reason: " + i); - }; - - controller.FetchAdditionalProducts(additional, onSuccess, onFailure); + var additional = new HashSet() { + new ProductDefinition("coins.500", ProductType.Consumable), + new ProductDefinition("armour", ProductType.NonConsumable) + }; + + Action onSuccess = () => { + Debug.Log("Fetched successfully!"); + // The additional products are added to the set of + // previously retrieved products and are browseable + // and purchasable. + foreach (var product in controller.products.all) { + Debug.Log(product.definition.id); + } + }; + + Action onFailure = (InitializationFailureReason i) => { + Debug.Log("Fetching failed for the specified reason: " + i); + }; + + controller.FetchAdditionalProducts(additional, onSuccess, onFailure); } ```` diff --git a/Documentation~/UnityIAPGoogleConfiguration.md b/Documentation~/UnityIAPGoogleConfiguration.md index d570a7c..97a6dd9 100644 --- a/Documentation~/UnityIAPGoogleConfiguration.md +++ b/Documentation~/UnityIAPGoogleConfiguration.md @@ -2,9 +2,9 @@ ## Introduction -This guide describes the process of establishing the digital records and relationships necessary for a Unity game to interact with an In-App Purchase Store. +This guide describes the process of establishing the digital records and relationships necessary for a Unity game to interact with an In-App Purchase Store. -In-App Purchase (IAP) is the process of transacting money for digital goods. A platform's Store allows the purchase of Products, representing digital goods. These Products have an Identifier, typically of string datatype. Products have Types to represent their durability: _subscription_, _consumable_ (capable of being rebought), and _non-consumable_ (capable of being bought once) are the most common. +In-App Purchase (IAP) is the process of transacting money for digital goods. A platform's Store allows the purchase of Products, representing digital goods. These Products have an Identifier, typically of string datatype. Products have Types to represent their durability: _subscription_, _consumable_ (capable of being rebought), and _non-consumable_ (capable of being bought once) are the most common. ## Google Play Store @@ -12,13 +12,13 @@ In-App Purchase (IAP) is the process of transacting money for digital goods. A p 1. Write a game implementing Unity IAP. See [Unity IAP Initialization](Overview.md) and [the Sample IAP Project](https://forum.unity.com/threads/sample-iap-project.529555/). -2. Keep the game's product identifiers on-hand for Google Play Developer Console use later. +2. Keep the game's product identifiers on-hand for Google Play Developer Console use later. ![gold50](images/IAPGoogleImage0.png) -3. Build a signed non-Development Build Android APK from your game. +3. Build a signed non-Development Build Android APK from your game. - **TIP:** Make sure you safely store your keystore file. The original keystore is always required to update a published Google Play application. + **TIP:** Make sure you safely store your keystore file. The original keystore is always required to update a published Google Play application. **TIP:** Reuse the Bundle Version Code from your last uploaded APK during local testing to permit side-loading without first being required to upload the changed APK to the Developer Console. See the settings for the Android platform Player. @@ -26,13 +26,13 @@ In-App Purchase (IAP) is the process of transacting money for digital goods. A p From the Google Account that will publish the game, register the Android application with the [Google Play Developer Console](https://play.google.com/apps/publish). -**NOTE:** This guide uses the [Google Play License Testing approach](http://developer.android.com/google/play/billing/billing_testing.html) for testing in-app purchase integration. +**NOTE:** This guide uses the [Google Play License Testing approach](http://developer.android.com/google/play/billing/billing_testing.html) for testing in-app purchase integration. 1. Choose __Create app__. ![All apps](images/IAPGoogleImage1.png) -2. Give the application an App name and select the appropriate options for your game. +2. Give the application an App name and select the appropriate options for your game. ![Create app](images/IAPGoogleImage2.png) @@ -48,26 +48,28 @@ Now that you have uploaded our first binary, you can add the IAP products. ![In-app products](images/IAPGoogleImage4.png) -2. Define the __Product ID__ , product details and price. Remember to activate the product after saving. +2. Define the __Product ID__ , product details and price. Remember to activate the product after saving. You can specify a consumable or non-consumable Product Type in __Managed product__. __Subscription__ is also supported by Unity IAP. -**NOTE**: The "Product ID" here is the same identifier used in the game source code, added to the [Unity IAP ConfigurationBuilder] instance via `AddProduct()` or `AddProducts()`, like "gold50". +**NOTE**: The "Product ID" here is the same identifier used in the game source code, added to the [Unity IAP ConfigurationBuilder] instance via `AddProduct()` or `AddProducts()`, like "gold50". ![50goldcoins](images/IAPGoogleImage5.png) +**WARNING:** multi-quantity is not supported yet and should not be enabled. + ### Test IAP -Add your testers to License Testing. +Add your testers to License Testing. -1. Navigate to All Apps on your Google Developer dashboard. +1. Navigate to All Apps on your Google Developer dashboard. -2. Select __Settings/License Testing__. Add each Google Account email address. Save changes. +2. Select __Settings/License Testing__. Add each Google Account email address. Save changes. ![License testing](images/IAPGoogleImage6.png) - NOTE: There may be a delay of several hours from the time you publish the APK. - + NOTE: There may be a delay of several hours from the time you publish the APK. + 3. When available, share the __Join on Android__ link with testers. Ensure that testers can install the application from the store. __Note:__ To test updates retaining permission to purchase IAPS's for free, you may side-load applications, updating the existing store-downladed APK install. @@ -78,5 +80,5 @@ __Note:__ To test updates retaining permission to purchase IAPS's for free, you 4. To test the IAP, make a purchase on a device logged in with a Tester Google Account. A modified purchase dialog box appears to confirm the fact this product is under test and is free. **WARNING**: If this dialog box does not appear, then the Tester Google Account will be charged real money for the product. - + ![](images/IAPGoogleImage8.png) diff --git a/Documentation~/UnityIAPGooglePlay.md b/Documentation~/UnityIAPGooglePlay.md index 5efab53..c2b320c 100644 --- a/Documentation~/UnityIAPGooglePlay.md +++ b/Documentation~/UnityIAPGooglePlay.md @@ -3,7 +3,7 @@ Consumables ----------- -Unity IAP uses V3 of Google's Billing API, which features the concept of consumable products and explicit consumption API calls. +Unity IAP uses V4 of Google's Billing API, which features the concept of consumable products and explicit consumption API calls. When you create consumable products in the Google Publisher dashboard, set them to be 'Managed' products. Unity IAP will take care of consuming them after your application has confirmed a purchase. @@ -12,7 +12,7 @@ Extended functionality ### Listen for recoverable initialization interruptions -A game may not complete initializing Unity IAP, either successfully or unsuccessfully, in certain circumstances. This can be due to the user having no Google account added to their Android device when the game initializes Unity IAP. +A game may not complete initializing Unity IAP, either successfully or unsuccessfully, in certain circumstances. This can be due to the user having no Google account added to their Android device when the game initializes Unity IAP. For example: a user first installs the app with the Play Store. Then the user removes their Google account from the device. The user launches the game and Unity IAP does not finish initializing, preventing the user from purchasing or restoring any prior purchases. To fix this, the user can [add a Google account](https://support.google.com/android/answer/7664951) to their device and return to the game. @@ -50,3 +50,43 @@ public class GooglePlayInitializationDisconnectListener : IStoreListener public void OnPurchaseFailed(Product i, PurchaseFailureReason p) { } } ``` + +### Listen for failed query product details + +Querying product details from the Google Play Store can fail due to certain circumstances. When this happens, we retry until successful. + +For example: a user first installs the app with the Play Store. Then the user launches the app without having Internet access. The Google Play Store will be unavailable because it requires an Internet connection which will result in failing to query product details. Restoring the Internet connection will fix the problem and the app will resume correctly. + +The `IGooglePlayConfiguration.SetQueryProductDetailsFailedListener(Action)` API can be used to listen for this scenario. The action has a parameter which contains the retry count. When this Action is triggered, the app may choose to advise the user through a user interface dialog to verify their Internet connection. + +Please refer to this usage sample: + +``` +using UnityEngine; +using UnityEngine.Purchasing; + +public class QueryProductDetailsFailedListener : IStoreListener +{ + public QueryProductDetailsFailedListener() + { + var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance()); + builder.Configure().SetQueryProductDetailsFailedListener((int retryCount) => + { + Debug.Log("Failed to query product details " + retryCount + " times."); + }); + builder.AddProduct("100_gold_coins", ProductType.Consumable); + UnityPurchasing.Initialize(this, builder); + } + + public void OnInitialized(IStoreController controller, IExtensionProvider extensions) { } + + public void OnInitializeFailed(InitializationFailureReason error) { } + + public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e) + { + return PurchaseProcessingResult.Complete; + } + + public void OnPurchaseFailed(Product i, PurchaseFailureReason p) { } +} +``` diff --git a/Documentation~/UnityIAPHandlingPurchaseFailures.md b/Documentation~/UnityIAPHandlingPurchaseFailures.md index 1a098ab..2c52fb4 100644 --- a/Documentation~/UnityIAPHandlingPurchaseFailures.md +++ b/Documentation~/UnityIAPHandlingPurchaseFailures.md @@ -14,4 +14,3 @@ public void OnPurchaseFailed (Product i, PurchaseFailureReason p) } } ```` - diff --git a/Documentation~/UnityIAPIStoreHandlingPurchases.md b/Documentation~/UnityIAPIStoreHandlingPurchases.md index 0162257..2640e90 100644 --- a/Documentation~/UnityIAPIStoreHandlingPurchases.md +++ b/Documentation~/UnityIAPIStoreHandlingPurchases.md @@ -11,6 +11,3 @@ Finishing Transactions When the application acknowledges that a transaction has been processed, or if the transaction has already been processed, Unity IAP invokes your store’s FinishTransaction method. Stores should use FinishTransaction to perform any housekeeping following a purchase, such as closing transactions or consuming consumable products. - - - diff --git a/Documentation~/UnityIAPIStoreInitialization.md b/Documentation~/UnityIAPIStoreInitialization.md index 897f4ca..a914486 100644 --- a/Documentation~/UnityIAPIStoreInitialization.md +++ b/Documentation~/UnityIAPIStoreInitialization.md @@ -9,4 +9,3 @@ void Initialize(IStoreCallback callback) { this.callback = callback; } ```` - diff --git a/Documentation~/UnityIAPIStoreRetrievingProducts.md b/Documentation~/UnityIAPIStoreRetrievingProducts.md index 8936fd9..6ad4c37 100644 --- a/Documentation~/UnityIAPIStoreRetrievingProducts.md +++ b/Documentation~/UnityIAPIStoreRetrievingProducts.md @@ -13,4 +13,3 @@ Handling errors --------------- If products cannot be retrieved due to an unrecoverable error, such as the developer making an error with their store configuration, you should call the ``OnSetupFailed`` method of the ``IStoreCallback``, indicating the ``InitializationFailureReason`` responsible. - diff --git a/Documentation~/UnityIAPImplementingAStore.md b/Documentation~/UnityIAPImplementingAStore.md index 0e8c0fb..78d595b 100644 --- a/Documentation~/UnityIAPImplementingAStore.md +++ b/Documentation~/UnityIAPImplementingAStore.md @@ -11,7 +11,7 @@ public class MyStore : IStore private IStoreCallback callback; public void Initialize (IStoreCallback callback) { - this.callback = callback; + this.callback = callback; } public void RetrieveProducts (System.Collections.ObjectModel.ReadOnlyCollection products) @@ -26,8 +26,7 @@ public class MyStore : IStore public void FinishTransaction (UnityEngine.Purchasing.ProductDefinition product, string transactionId) { - // Perform transaction related housekeeping + // Perform transaction related housekeeping } } ```` - diff --git a/Documentation~/UnityIAPInitializeUnityGamingServices.md b/Documentation~/UnityIAPInitializeUnityGamingServices.md new file mode 100644 index 0000000..349f99e --- /dev/null +++ b/Documentation~/UnityIAPInitializeUnityGamingServices.md @@ -0,0 +1,63 @@ +# Initialize Unity Gaming Services + +Call `UnityServices.InitializeAsync()` to initialize all **Unity Gaming Services** at once. +It returns a `Task` that enables you to monitor the initialization's progression. + +#### Example +```cs +using System; +using Unity.Services.Core; +using Unity.Services.Core.Environments; +using UnityEngine; + +public class InitializeUnityServices : MonoBehaviour +{ + public string environment = "production"; + + async void Start() + { + try + { + var options = new InitializationOptions() + .SetEnvironmentName(environment); + + await UnityServices.InitializeAsync(options); + } + catch (Exception exception) + { + // An error occurred during services initialization. + } + } +} +``` + +### Automatic initialization for Codeless IAP + +If you are using the Codeless IAP, you may instead enable **Unity Gaming Services** automatic initialization by checking the **Automatically initialize Unity Gaming Services** checkbox at the bottom of the **IAP Catalog** window. +This ensures that **Unity Gaming Services** initializes immediately when the application starts. +![Enabling auto-initialization for the Unity Gaming Services through the **IAP Catalog** GUI](images/AutoInitializeUGS.png) +To use this feature **Automatically initialize UnityPurchasing (recommended)** must be enabled. If you do not see these checkboxes inside the **IAP Catalog**, it may be because you have not yet added products in the catalog window. + +This initializes **Unity Gaming Services** with the default `production` environment. +This way of initializing **Unity Gaming Services** might not be compatible with all other services as they might require special initialization options. +If the use of initialization options is needed, **Unity Gaming Services** should be initialized with the coded API as described above. + +## Warning message + +If you attempt to use the **Unity IAP** service without first initializing **Unity Gaming Services**, you will receive the following warning message: +``` +Unity In-App Purchasing requires Unity Gaming Services to have been initialized before use. +Find out how to initialize Unity Gaming Services by following the documentation https://docs.unity.com/ugs-overview/services-core-api.html#InitializationExample +or download the 06 Initialize Gaming Services sample from Package Manager > In-App Purchasing > Samples. +``` + +## Technical details + +The `InitializeAsync` methods affect the currently installed service packages in your Unity project. + +Note that this method is not supported during edit time. + +___ +For more information, please see the [Services Core API documentation](https://docs.unity.com/ugs-overview/services-core-api.html#Services_Core_API). + +Download the `06 Initialize Gaming Services` from `Package Manager > In-App Purchasing > Samples` for a concrete example. diff --git a/Documentation~/UnityIAPInitiatingPurchases.md b/Documentation~/UnityIAPInitiatingPurchases.md index ff0936c..30a90f5 100644 --- a/Documentation~/UnityIAPInitiatingPurchases.md +++ b/Documentation~/UnityIAPInitiatingPurchases.md @@ -11,4 +11,3 @@ public void OnPurchaseClicked(string productId) { ```` Your application will be notified asynchronously of the result, either with an invocation of ``ProcessPurchase`` for successful purchases or ``OnPurchaseFailed`` for failures. - diff --git a/Documentation~/UnityIAPModuleConfiguration.md b/Documentation~/UnityIAPModuleConfiguration.md index 778dca9..56761b0 100644 --- a/Documentation~/UnityIAPModuleConfiguration.md +++ b/Documentation~/UnityIAPModuleConfiguration.md @@ -9,4 +9,3 @@ BindConfiguration(new MyConfiguration()); ```` When developers request an instance of your configuration type, Unity IAP first tries to cast your store implementation to the configuration type. Only if that cast fails will any instance bound via ``BindConfiguration`` will be used. - diff --git a/Documentation~/UnityIAPModuleExtension.md b/Documentation~/UnityIAPModuleExtension.md index dfca489..4fefea3 100644 --- a/Documentation~/UnityIAPModuleExtension.md +++ b/Documentation~/UnityIAPModuleExtension.md @@ -20,5 +20,3 @@ Applications request extended functionality via the ``IExtensionProvider``. When If that cast fails, Unity IAP will provide any instance registered via a call your store module has provided via ``RegisterExtension``, or null if no instance has been provided. Modules should provide instances for the extension interfaces they define even when running on unsupported platforms, so as to avoid forcing application developers to use platform dependent compilation. - - diff --git a/Documentation~/UnityIAPModuleRegistration.md b/Documentation~/UnityIAPModuleRegistration.md index 01cd310..804851d 100644 --- a/Documentation~/UnityIAPModuleRegistration.md +++ b/Documentation~/UnityIAPModuleRegistration.md @@ -17,5 +17,3 @@ private void InstantiateMyStore() { ```` The store name must match the name developers use when defining products for your store so Unity IAP uses the correct product identifiers when addressing your store. - - diff --git a/Documentation~/UnityIAPModules.md b/Documentation~/UnityIAPModules.md index e8e5e3a..ae61844 100644 --- a/Documentation~/UnityIAPModules.md +++ b/Documentation~/UnityIAPModules.md @@ -12,4 +12,3 @@ ConfigurationBuilder.Instance (MyCustomModule.Instance(), StandardPurchasingModu Where two or more modules have implementations available for a given platform, precedence is given in order the modules were supplied to the ``ConfigurationBuilder``; any implementation provided by ``MyCustomModule`` will be used in preference to ``StandardPurchasingModule``. Note that a module can support multiple stores; the ``StandardPurchasingModule`` handles all of Unity IAPs default store implementations. - diff --git a/Documentation~/UnityIAPProcessingPurchases.md b/Documentation~/UnityIAPProcessingPurchases.md index 73e7791..e512d61 100644 --- a/Documentation~/UnityIAPProcessingPurchases.md +++ b/Documentation~/UnityIAPProcessingPurchases.md @@ -32,5 +32,3 @@ If you are saving consumable purchases to the cloud, you **must** return `Purcha When returning `Pending`, Unity IAP keeps transactions open on the underlying store until confirmed as processed, ensuring consumable purchases are not lost even if a user reinstalls your application while a consumable is in this state. ![Pending Purchases](images/PurchaseProcessingResult.Pending.png) - - diff --git a/Documentation~/UnityIAPPurchaseReceipts.md b/Documentation~/UnityIAPPurchaseReceipts.md index 104b21b..2fadcdc 100644 --- a/Documentation~/UnityIAPPurchaseReceipts.md +++ b/Documentation~/UnityIAPPurchaseReceipts.md @@ -1,4 +1,4 @@ -# Purchase Receipts +# Purchase Receipts Unity IAP provides purchase receipts as a JSON hash containing the following keys and values: diff --git a/Documentation~/UnityIAPRestoringTransactions.md b/Documentation~/UnityIAPRestoringTransactions.md index 0c4e3a8..037bc66 100644 --- a/Documentation~/UnityIAPRestoringTransactions.md +++ b/Documentation~/UnityIAPRestoringTransactions.md @@ -12,14 +12,13 @@ On Apple platforms users must enter their password to retrieve previous transact /// public void OnInitialized(IStoreController controller, IExtensionProvider extensions) { - extensions.GetExtension ().RestoreTransactions (result => { - if (result) { - // This does not mean anything was restored, - // merely that the restoration process succeeded. - } else { - // Restoration failed. - } - }); + extensions.GetExtension ().RestoreTransactions ((result, error) => { + if (result) { + // This does not mean anything was restored, + // merely that the restoration process succeeded. + } else { + // Restoration failed. `error` contains the failure reason. + } + }); } ```` - diff --git a/Documentation~/UnityIAPUniversalWindows.md b/Documentation~/UnityIAPUniversalWindows.md index 69898e4..c37aee6 100644 --- a/Documentation~/UnityIAPUniversalWindows.md +++ b/Documentation~/UnityIAPUniversalWindows.md @@ -11,4 +11,3 @@ var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance()) builder.Configure().useMockBillingSystem = true; ```` Make sure you disable the mock billing system before publishing your application. - diff --git a/Documentation~/UnityIAPValidatingReceipts.md b/Documentation~/UnityIAPValidatingReceipts.md index 5c7a038..8f6ba99 100644 --- a/Documentation~/UnityIAPValidatingReceipts.md +++ b/Documentation~/UnityIAPValidatingReceipts.md @@ -32,6 +32,8 @@ The `CrossPlatformValidator` performs two checks: Note that the validator only validates receipts generated on Google Play and Apple platforms. Receipts generated on any other platform, including fakes generated in the Editor, throw an __IAPSecurityException__. +Be sure that your `CrossPlatformValidator` object has been created in time for processing your purchases. Note that during the initialization of Unity IAP, it is possible that pending purchases from previous sessions may be fetched from the store and processed. If you are using a persistent object of this type, create it before initializing Unity IAP. + If you try to validate a receipt for a platform that you haven't supplied a secret key for, a __MissingStoreSecretException__ is thrown. ```` @@ -44,7 +46,7 @@ public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e) // Prepare the validator with the secrets we prepared in the Editor // obfuscation window. var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), - AppleTangle.Data(), Application.bundleIdentifier); + AppleTangle.Data(), Application.identifier); try { // On Google Play, result has a single product ID. @@ -74,16 +76,16 @@ public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e) ### Choose an Apple certificate: Apple Root or StoreKit Test -(*) Unity IAP supports receipt validation of purchases made with the StoreKit Test store simulation. +(*) Unity IAP supports receipt validation of purchases made with the StoreKit Test store simulation. Apple's Xcode 12 offers the ["StoreKit Test"](https://developer.apple.com/documentation/Xcode/setting-up-storekit-testing-in-xcode) suite of features for developers to more conveniently test IAP, without the need to use an Apple App Store Connect Sandbox configuration. -Use the `AppleStoreKitTestTangle` class in place of the usual `AppleTangle` class, when constructing the `CrossPlatformValidator` for receipt validation. Note that both tangle classes are generated by the **Receipt Validation Obfuscator**. +Use the `AppleStoreKitTestTangle` class in place of the usual `AppleTangle` class, when constructing the `CrossPlatformValidator` for receipt validation. Note that both tangle classes are generated by the **Receipt Validation Obfuscator**. ```` public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e) { - bool validPurchase = true; + bool validPurchase = true; #if UNITY_ANDROID || UNITY_IOS || UNITY_STANDALONE_OSX // Choose one Apple certificate. NOTE AppleStoreKitTestTangle requires @@ -94,10 +96,10 @@ public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e) #if !DEBUG_STOREKIT_TEST var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), - AppleTangle.Data(), Application.bundleIdentifier); + AppleTangle.Data(), Application.identifier); #else var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), - AppleStoreKitTestTangle.Data(), Application.bundleIdentifier); + AppleStoreKitTestTangle.Data(), Application.identifier); #endif try { @@ -127,27 +129,27 @@ Different stores have different fields in their purchase receipts. To access sto var result = validator.Validate(e.purchasedProduct.receipt); Debug.Log("Receipt is valid. Contents:"); foreach (IPurchaseReceipt productReceipt in result) { - Debug.Log(productReceipt.productID); - Debug.Log(productReceipt.purchaseDate); + Debug.Log(productReceipt.productID); + Debug.Log(productReceipt.purchaseDate); Debug.Log(productReceipt.transactionID); - GooglePlayReceipt google = productReceipt as GooglePlayReceipt; - if (null != google) { - // This is Google's Order ID. - // Note that it is null when testing in the sandbox - // because Google's sandbox does not provide Order IDs. - Debug.Log(google.transactionID); - Debug.Log(google.purchaseState); - Debug.Log(google.purchaseToken); - } - - AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt; - if (null != apple) { - Debug.Log(apple.originalTransactionIdentifier); - Debug.Log(apple.subscriptionExpirationDate); - Debug.Log(apple.cancellationDate); - Debug.Log(apple.quantity); - } + GooglePlayReceipt google = productReceipt as GooglePlayReceipt; + if (null != google) { + // This is Google's Order ID. + // Note that it is null when testing in the sandbox + // because Google's sandbox does not provide Order IDs. + Debug.Log(google.transactionID); + Debug.Log(google.purchaseState); + Debug.Log(google.purchaseToken); + } + + AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt; + if (null != apple) { + Debug.Log(apple.originalTransactionIdentifier); + Debug.Log(apple.subscriptionExpirationDate); + Debug.Log(apple.cancellationDate); + Debug.Log(apple.quantity); + } } ```` @@ -166,8 +168,8 @@ AppleReceipt receipt = new AppleValidator(AppleTangle.Data()).Validate(receiptDa Debug.Log(receipt.bundleID); Debug.Log(receipt.receiptCreationDate); foreach (AppleInAppPurchaseReceipt productReceipt in receipt.inAppPurchaseReceipts) { - Debug.Log(productReceipt.transactionIdentifier); - Debug.Log(productReceipt.productIdentifier); + Debug.Log(productReceipt.transactionIdentifier); + Debug.Log(productReceipt.productIdentifier); } #endif ```` diff --git a/Documentation~/UnityIAPWindowsConfiguration.md b/Documentation~/UnityIAPWindowsConfiguration.md index 13c0038..f4dcd7e 100644 --- a/Documentation~/UnityIAPWindowsConfiguration.md +++ b/Documentation~/UnityIAPWindowsConfiguration.md @@ -4,15 +4,15 @@ This guide describes the process of establishing the digital records and relationships necessary for a Unity game to interact with an In-App Purchase Store. -In-App Purchase (IAP) is the process of transacting money for digital goods. A platform's Store allows purchase of Products representing digital goods. These Products have an Identifier, typically of string datatype. Products have Types to represent their durability: _subscription_, _consumable_ (capable of being rebought), and _non-consumable_ (capable of being bought once) are the most common. +In-App Purchase (IAP) is the process of transacting money for digital goods. A platform's Store allows purchase of Products representing digital goods. These Products have an Identifier, typically of string datatype. Products have Types to represent their durability: _subscription_, _consumable_ (capable of being rebought), and _non-consumable_ (capable of being bought once) are the most common. ## Windows Store ### Introduction -Windows App Development offers both local and remote Windows Store client-server IAP testing. +Windows App Development offers both local and remote Windows Store client-server IAP testing. -This page covers local testing with the emulator and a simulated billing system, then Windows Store testing which limits app publication visibility to those with the app's link. +This page covers local testing with the emulator and a simulated billing system, then Windows Store testing which limits app publication visibility to those with the app's link. **NOTE**: This guide targets Windows 10 Universal SDK. Other Windows targets are available. @@ -22,19 +22,19 @@ This page covers local testing with the emulator and a simulated billing system, 1. Write a game implementing Unity IAP. See [Unity IAP Initialization](Overview.md). -1. Keep the game's product identifiers on-hand for use in Microsoft's Windows Dev Center Dashboard to perform remote Windows Store testing later. +1. Keep the game's product identifiers on-hand for use in Microsoft's Windows Dev Center Dashboard to perform remote Windows Store testing later. ![](images/IAPWindowsImage0.png) ### Test IAP locally -Microsoft offers a simulated billing system, permitting local testing of IAP. This removes the need to configure anything on the Windows Dev Center or communicate with the the Windows Store via the app for initial integration testing. +Microsoft offers a simulated billing system, permitting local testing of IAP. This removes the need to configure anything on the Windows Dev Center or communicate with the the Windows Store via the app for initial integration testing. -[Configuring local testing](UnityIAPUniversalWindows.md) is far simpler than for remote Store testing, although it requires temporary code changes to the app which need to be removed before app publication. +[Configuring local testing](UnityIAPUniversalWindows.md) is far simpler than for remote Store testing, although it requires temporary code changes to the app which need to be removed before app publication. To test IAP locally: -1. Enable the simulated billing system in code where Unity IAP is initialized with its ConfigurationBuilder instance. +1. Enable the simulated billing system in code where Unity IAP is initialized with its ConfigurationBuilder instance. **WARNING**: Remove these code changes after testing, before publishing to the Store; otherwise the app will not transact any real money via IAP! @@ -52,7 +52,7 @@ To test IAP locally: Once basic IAP functionality has been tested locally, you can more confidently begin working with the Windows Store. This test confirms that the app has all necessary IAPs registered correctly to permit purchasing. -For testing IAP and publication use the [Windows Dev Center](https://dev.windows.com/en-us/publish) and configure the app with a limited visibility. This limits the app's visibility to those who have its direct link. +For testing IAP and publication use the [Windows Dev Center](https://dev.windows.com/en-us/publish) and configure the app with a limited visibility. This limits the app's visibility to those who have its direct link. **NOTE**: Testing on the Store also requires Certification, which may serve as an obstacle to testing. It is therefore important to complete testing locally before proceeding to testing with Windows Store. @@ -60,7 +60,7 @@ For testing IAP and publication use the [Windows Dev Center](https://dev.windows ![](images/IAPWindowsImage2.png) -2. Reserve the app name. +2. Reserve the app name. ![](images/IAPWindowsImage3.png) @@ -68,21 +68,21 @@ For testing IAP and publication use the [Windows Dev Center](https://dev.windows ![](images/IAPWindowsImage4.png) -4. In "Distribution and visibility" see a list of the Store's available [publication behaviors](https://msdn.microsoft.com/en-us/library/windows/apps/mt148548.aspx#dist_vis). Select __Hide this app in the Store__. +4. In "Distribution and visibility" see a list of the Store's available [publication behaviors](https://msdn.microsoft.com/en-us/library/windows/apps/mt148548.aspx#dist_vis). Select __Hide this app in the Store__. ![](images/IAPWindowsImage5.png) -5. Collect the direct link. This will be used to install the app on a Windows 10 device for [testing](https://msdn.microsoft.com/en-us/library/windows/apps/mt148561.aspx). +5. Collect the direct link. This will be used to install the app on a Windows 10 device for [testing](https://msdn.microsoft.com/en-us/library/windows/apps/mt148561.aspx). ![](images/IAPWindowsImage6.png) -6. Submit the app for Certification. +6. Submit the app for Certification. Submissions may take many hours to complete, and blocking issues may be raised by Microsoft Certification, which you will need to address before the submission passes successfully. ### Add In-App Products on the Store -Add each IAP, setting the price to be "free" so that no money will be transacted during testing. After the test is completed, reconfigure the IAP with the desired price and republish it. See [IAP Submissions](https://msdn.microsoft.com/en-us/library/windows/apps/mt148551.aspx). +Add each IAP, setting the price to be "free" so that no money will be transacted during testing. After the test is completed, reconfigure the IAP with the desired price and republish it. See [IAP Submissions](https://msdn.microsoft.com/en-us/library/windows/apps/mt148551.aspx). 1. In the new app's "App overview" page, click __Create a new IAP__ . @@ -92,7 +92,7 @@ Add each IAP, setting the price to be "free" so that no money will be transacted ![](images/IAPWindowsImage8.png) -3. Configure the type, price, and language. +3. Configure the type, price, and language. **NOTE**: For **Pricing and availability** choose **free** for testing purposes to avoid incurring unnecessary financial charges. When you're finished with testing, update and re-submit each IAP with the desired price in preparation for release to the public. @@ -113,12 +113,12 @@ Add each IAP, setting the price to be "free" so that no money will be transacted Select the declared language when returned to the IAP overview. ![](images/IAPWindowsImage13.png) - - Populate the Title, Description and Icon. + + Populate the Title, Description and Icon. ![](images/IAPWindowsImage14.png) -4. Submit the IAP for Certification. +4. Submit the IAP for Certification. As with apps, IAP submissions may take many hours to complete, and blocking issues may be raised by Microsoft Certification which you will need to address before the submission passes successfully. @@ -135,5 +135,3 @@ These steps follow a branch of the beta test process made possible with Windows 3. Test IAP. 4. After passing test, update the IAP with the desired public pricing, update the app visibility settings to share with the general public, and submit both kinds of changes for final Certification. - - diff --git a/Documentation~/UnityIAPiOSMAS.md b/Documentation~/UnityIAPiOSMAS.md index f93213b..534e71c 100644 --- a/Documentation~/UnityIAPiOSMAS.md +++ b/Documentation~/UnityIAPiOSMAS.md @@ -32,14 +32,14 @@ On Apple platforms users must enter their password to retrieve previous transact /// public void OnInitialized(IStoreController controller, IExtensionProvider extensions) { - extensions.GetExtension ().RestoreTransactions (result => { - if (result) { - // This does not mean anything was restored, - // merely that the restoration process succeeded. - } else { - // Restoration failed. - } - }); + extensions.GetExtension ().RestoreTransactions ((result, error) => { + if (result) { + // This does not mean anything was restored, + // merely that the restoration process succeeded. + } else { + // Restoration failed. `error` contains the failure reason. + } + }); } ```` @@ -57,22 +57,22 @@ Unity IAP makes this method available as follows: /// public void OnInitialized(IStoreController controller, IExtensionProvider extensions) { - extensions.GetExtension ().RefreshAppReceipt (receipt => { - // This handler is invoked if the request is successful. - // Receipt will be the latest app receipt. - Console.WriteLine(receipt); - }, - () => { - // This handler will be invoked if the request fails, - // such as if the network is unavailable or the user - // enters the wrong password. - }); + extensions.GetExtension ().RefreshAppReceipt (receipt => { + // This handler is invoked if the request is successful. + // Receipt will be the latest app receipt. + Console.WriteLine(receipt); + }, + () => { + // This handler will be invoked if the request fails, + // such as if the network is unavailable or the user + // enters the wrong password. + }); } ```` ### Ask to Buy -iOS 8 introduced a new parental control feature called [Ask to Buy](https://developer.apple.com/library/ios/technotes/tn2259/_index.html#/apple_ref/doc/uid/DTS40009578-CH1-UPDATE_YOUR_APP_FOR_ASK_TO_BUY). +iOS 8 introduced a new parental control feature called [Ask to Buy](https://developer.apple.com/library/ios/technotes/tn2259/_index.html#/apple_ref/doc/uid/DTS40009578-CH1-UPDATE_YOUR_APP_FOR_ASK_TO_BUY). Ask to Buy purchases defer for parent approval. When this occurs, Unity IAP sends your app a notification as follows: @@ -95,19 +95,19 @@ using UnityEngine; using UnityEngine.Purchasing; public class AppleSimulateAskToBuy : MonoBehaviour { - public void SetSimulateAskToBuy(bool shouldSimulateAskToBuy) { - if (Application.platform == RuntimePlatform.IPhonePlayer) { - IAppleExtensions extensions = IAPButton.IAPButtonStoreManager.Instance.ExtensionProvider.GetExtension(); - extensions.simulateAskToBuy = shouldSimulateAskToBuy; - } - } + public void SetSimulateAskToBuy(bool shouldSimulateAskToBuy) { + if (Application.platform == RuntimePlatform.IPhonePlayer) { + IAppleExtensions extensions = IAPButton.IAPButtonStoreManager.Instance.ExtensionProvider.GetExtension(); + extensions.simulateAskToBuy = shouldSimulateAskToBuy; + } + } } ``` When the purchase is approved or rejected, your store's normal `ProcessPurchase` or `OnPurchaseFailed` listener methods are invoked. ### Transaction Receipts -Sometimes consumable Ask to Buy purchases don't show up in the App Receipt, in which case you cannot validate them using that receipt. However, iOS provides a Transaction Receipt that contains all purchases, including Ask to Buy. Access the most recent Transaction Receipt string for a given `Product` using `IAppleExtensions`. +Sometimes consumable Ask to Buy purchases don't show up in the App Receipt, in which case you cannot validate them using that receipt. However, iOS provides a Transaction Receipt that contains all purchases, including Ask to Buy. Access the most recent Transaction Receipt string for a given `Product` using `IAppleExtensions`. **Note**: Transaction Receipts are not available for Mac builds. Requesting a Transaction Receipt on a Mac build results in an empty string. @@ -156,7 +156,7 @@ public class AskToBuy : MonoBehaviour, IStoreListener string transactionReceipt = m_AppleExtensions.GetTransactionReceiptForProduct (e.purchasedProduct); Console.WriteLine (transactionReceipt); // Send transaction receipt to server for validation - } + } return PurchaseProcessingResult.Complete; } @@ -223,11 +223,11 @@ Example JSON response: ``` ## Intercepting Apple promotional purchases -Apple allows you to promote [in-game purchases](https://developer.apple.com/app-store/promoting-in-app-purchases/#:~:text=inside%20your%20app.-,Overview,approved%20and%20ready%20to%20promote.&text=When%20a%20user%20doesn't,to%20download%20the%20app%20first.) through your app’s product page. Unlike conventional in-app purchases, Apple promotional purchases initiate directly from the App Store on iOS and tvOS. The App Store then launches your app to complete the transaction, or prompts the user to download the app if it isn’t installed. +Apple allows you to promote [in-game purchases](https://developer.apple.com/app-store/promoting-in-app-purchases/#:~:text=inside%20your%20app.-,Overview,approved%20and%20ready%20to%20promote.&text=When%20a%20user%20doesn't,to%20download%20the%20app%20first.) through your app’s product page. Unlike conventional in-app purchases, Apple promotional purchases initiate directly from the App Store on iOS and tvOS. The App Store then launches your app to complete the transaction, or prompts the user to download the app if it isn’t installed. The `IAppleConfiguration` `SetApplePromotionalPurchaseInterceptor` callback method intercepts Apple promotional purchases. Use this callback to present parental gates, send analytics events, or perform other functions before sending the purchase to Apple. The callback uses the `Product` that the user attempted to purchase. You must call `IAppleExtensions.ContinuePromotionalPurchases()` to continue with the promotional purchase. This will initiate any queued-up payments. -If you do not set the callback, promotional purchases go through immediately and call `ProcessPurchase` with the result. +If you do not set the callback, promotional purchases go through immediately and call `ProcessPurchase` with the result. **Note**: Calling these APIs on other platforms has no effect. @@ -249,7 +249,7 @@ public void Awake() { public void OnInitialized(IStoreController controller, IExtensionProvider extensions) { m_AppleExtensions = extensions.GetExtension(); foreach (var item in controller.products.all) { - if (item.availableToPurchase) { + if (item.availableToPurchase) { // Set all these products to be visible in the user's App Store m_AppleExtensions.SetStorePromotionVisibility(item, AppleStorePromotionVisibility.Show); } @@ -258,9 +258,9 @@ public void OnInitialized(IStoreController controller, IExtensionProvider extens private void OnPromotionalPurchase(Product item) { Debug.Log("Attempted promotional purchase: " + item.definition.id); - // Promotional purchase has been detected. + // Promotional purchase has been detected. // Handle this event by, e.g. presenting a parental gate. - // Here, for demonstration purposes only, we will wait five seconds before continuing + // Here, for demonstration purposes only, we will wait five seconds before continuing // the purchase. StartCoroutine(ContinuePromotionalPurchases()); } @@ -272,4 +272,4 @@ private IEnumerator ContinuePromotionalPurchases() { m_AppleExtensions.ContinuePromotionalPurchases (); // iOS and tvOS only } -``` \ No newline at end of file +``` diff --git a/Documentation~/WhatCustomStore.md b/Documentation~/WhatCustomStore.md index 2517bdc..58c8fd3 100644 --- a/Documentation~/WhatCustomStore.md +++ b/Documentation~/WhatCustomStore.md @@ -1,4 +1,3 @@ # What is a Custom Store A custom store is a store that the In-App Purchasing package doesn't support out of the out of the box. It allows developers to create their own stores and build on top of the existing infrastructure of the package. - diff --git a/Documentation~/WhatIsFakeStore.md b/Documentation~/WhatIsFakeStore.md index 2346607..c41ce34 100644 --- a/Documentation~/WhatIsFakeStore.md +++ b/Documentation~/WhatIsFakeStore.md @@ -1,3 +1,3 @@ # What is Fake Store? -Fake Store isn't a store that exists in production, it is used to tests your apps quickly inside the editor. \ No newline at end of file +Fake Store isn't a store that exists in production, it is used to tests your apps quickly inside the editor. diff --git a/Documentation~/images/AutoInitialize.png b/Documentation~/images/AutoInitialize.png index c192232..dfb9848 100644 Binary files a/Documentation~/images/AutoInitialize.png and b/Documentation~/images/AutoInitialize.png differ diff --git a/Documentation~/images/AutoInitializeUGS.png b/Documentation~/images/AutoInitializeUGS.png new file mode 100644 index 0000000..7899115 Binary files /dev/null and b/Documentation~/images/AutoInitializeUGS.png differ diff --git a/Documentation~/images/CodelessIAPButtonRestoreButton.png b/Documentation~/images/CodelessIAPButtonRestoreButton.png new file mode 100644 index 0000000..e0e6f12 Binary files /dev/null and b/Documentation~/images/CodelessIAPButtonRestoreButton.png differ diff --git a/Documentation~/images/CreateButton.png b/Documentation~/images/CreateButton.png index cb217c8..af9497e 100644 Binary files a/Documentation~/images/CreateButton.png and b/Documentation~/images/CreateButton.png differ diff --git a/Documentation~/images/UGSInitializationFlowDiagram.png b/Documentation~/images/UGSInitializationFlowDiagram.png new file mode 100644 index 0000000..14f8a2d Binary files /dev/null and b/Documentation~/images/UGSInitializationFlowDiagram.png differ diff --git a/Editor/AbstractIAPButtonEditor.cs b/Editor/AbstractIAPButtonEditor.cs new file mode 100644 index 0000000..2e4fd9b --- /dev/null +++ b/Editor/AbstractIAPButtonEditor.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEditor.Purchasing; +using UnityEngine; +using UnityEngine.Purchasing; + +namespace UnityEditor.Purchasing +{ + /// + /// Customer Editor class for the IAPButtons. This class handle how the IAPButtons should represent itself in the UnityEditor. + /// + public abstract class AbstractIAPButtonEditor : Editor + { + private static readonly string[] excludedFields = new string[] { "m_Script", "onTransactionsRestored" }; + private static readonly string[] restoreButtonExcludedFields = new string[] { "m_Script", "consumePurchase", "onPurchaseComplete", "onPurchaseFailed", "titleText", "descriptionText", "priceText" }; + private const string kNoProduct = ""; + + private readonly List m_ValidIDs = new List(); + private SerializedProperty m_ProductIDProperty; + + /// + /// Event trigger when IAPButton is enabled in the scene. + /// + protected void OnEnableInternal() + { + m_ProductIDProperty = serializedObject.FindProperty("productId"); + } + + /// + /// Event trigger when trying to draw the BaseIAPButton in the inspector. + /// + protected void OnInspectorGuiInternal() + { + var isAPurchaseButton = ((BaseIAPButton)target).IsAPurchaseButton(); + var productId = ((BaseIAPButton)target).GetProductId(); + DrawProductIdDropDown(isAPurchaseButton, productId); + } + + void DrawProductIdDropDown(bool isAPurchaseButton, string productId) + { + serializedObject.Update(); + + DrawProductIdDropdownWhenButtonIsPurchaseType(isAPurchaseButton, productId); + + DrawPropertiesExcluding(serializedObject, isAPurchaseButton ? excludedFields : restoreButtonExcludedFields); + + serializedObject.ApplyModifiedProperties(); + } + + void DrawProductIdDropdownWhenButtonIsPurchaseType(bool isAPurchaseButton, string productId) + { + if (isAPurchaseButton) + { + EditorGUILayout.LabelField(new GUIContent("Product ID:", "Select a product from the IAP catalog.")); + LoadProductIdsFromCodelessCatalog(); + m_ProductIDProperty.stringValue = GetCurrentlySelectedProduct(productId); + + if (GUILayout.Button("IAP Catalog...")) + { + ProductCatalogEditor.ShowWindow(); + } + } + } + + void LoadProductIdsFromCodelessCatalog() + { + var catalog = ProductCatalog.LoadDefaultCatalog(); + + m_ValidIDs.Clear(); + m_ValidIDs.Add(kNoProduct); + foreach (var product in catalog.allProducts) + { + m_ValidIDs.Add(product.id); + } + } + + string GetCurrentlySelectedProduct(string productId) + { + var currentIndex = string.IsNullOrEmpty(productId) ? 0 : m_ValidIDs.IndexOf(productId); + var newIndex = EditorGUILayout.Popup(currentIndex, m_ValidIDs.ToArray()); + return newIndex > 0 && newIndex < m_ValidIDs.Count ? m_ValidIDs[newIndex] : string.Empty; + } + } +} diff --git a/Editor/AbstractIAPButtonEditor.cs.meta b/Editor/AbstractIAPButtonEditor.cs.meta new file mode 100644 index 0000000..3144381 --- /dev/null +++ b/Editor/AbstractIAPButtonEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bb2c90e86b42468e815913090c486a0e +timeCreated: 1678482104 \ No newline at end of file diff --git a/Editor/Analytics/Entity/EventActions.cs b/Editor/Analytics/Entity/EventActions.cs index a4599bc..156c0ab 100644 --- a/Editor/Analytics/Entity/EventActions.cs +++ b/Editor/Analytics/Entity/EventActions.cs @@ -2,6 +2,7 @@ namespace UnityEditor.Purchasing { internal static class EventActions { + internal const string k_ActionAddIapButton_legacy = "legacy_add_iap_button"; internal const string k_ActionAddIapButton = "add_iap_button"; internal const string k_ActionAddIapListener = "add_iap_listener"; internal const string k_ActionAddPayout = "add_payout"; diff --git a/Editor/Analytics/Entity/EventUINames.cs b/Editor/Analytics/Entity/EventUINames.cs index 3d9567d..0022352 100644 --- a/Editor/Analytics/Entity/EventUINames.cs +++ b/Editor/Analytics/Entity/EventUINames.cs @@ -3,6 +3,7 @@ namespace UnityEditor.Purchasing internal static class EventUINames { internal const string k_UINameAutoInit = "auto_init_purchasing"; + internal const string k_UINameUgsAutoInit = "auto_init_ugs"; internal const string k_UINameSelectTargetStore = "select_target_store"; internal const string k_UINameProductType = "product_type"; internal const string k_UINamePayoutType = "payout_type"; diff --git a/Editor/Analytics/Entity/GameServices/GameServicesEventActions.cs b/Editor/Analytics/Entity/GameServices/GameServicesEventActions.cs index 97b1913..c6cbc5f 100644 --- a/Editor/Analytics/Entity/GameServices/GameServicesEventActions.cs +++ b/Editor/Analytics/Entity/GameServices/GameServicesEventActions.cs @@ -3,6 +3,7 @@ namespace UnityEditor.Purchasing static class GameServicesEventActions { internal const string k_ActionConfigure = "Configure"; + internal const string k_ActionCreateIapButton_legacy = "Legacy_CreateIapButton"; internal const string k_ActionCreateIapButton = "CreateIapButton"; internal const string k_ActionCreateIapListener = "CreateIapListener"; internal const string k_ActionIapCatalog = "IapCatalog"; diff --git a/Editor/Analytics/Helpers/GameServices/GameServicesEventSenderHelpers.cs b/Editor/Analytics/Helpers/GameServices/GameServicesEventSenderHelpers.cs index 9f087cc..910bed4 100644 --- a/Editor/Analytics/Helpers/GameServices/GameServicesEventSenderHelpers.cs +++ b/Editor/Analytics/Helpers/GameServices/GameServicesEventSenderHelpers.cs @@ -8,6 +8,11 @@ internal static void SendTopMenuConfigure() } internal static void SendTopMenuCreateIapButtonEvent() + { + BuildAndSendEvent(GameServicesEventComponents.k_ComponentTopMenu, GameServicesEventActions.k_ActionCreateIapButton_legacy); + } + + internal static void SendTopMenuCreateCodelessIapButtonEvent() { BuildAndSendEvent(GameServicesEventComponents.k_ComponentTopMenu, GameServicesEventActions.k_ActionCreateIapButton); } diff --git a/Editor/Analytics/Helpers/GenericEditorClickCheckboxEventSenderHelpers.cs b/Editor/Analytics/Helpers/GenericEditorClickCheckboxEventSenderHelpers.cs index bfaf86a..cb0a9d6 100644 --- a/Editor/Analytics/Helpers/GenericEditorClickCheckboxEventSenderHelpers.cs +++ b/Editor/Analytics/Helpers/GenericEditorClickCheckboxEventSenderHelpers.cs @@ -7,6 +7,11 @@ internal static void SendCatalogAutoInitToggleEvent(bool value) BuildAndSendEvent(EventComponents.k_ComponentCodeless, EventTools.k_ToolCatalog, EventUINames.k_UINameAutoInit, value); } + internal static void SendCatalogUgsAutoInitToggleEvent(bool value) + { + BuildAndSendEvent(EventComponents.k_ComponentCodeless, EventTools.k_ToolCatalog, EventUINames.k_UINameUgsAutoInit, value); + } + static void BuildAndSendEvent(string component, string tool, string name, bool value) { var newEvent = new GenericEditorClickCheckboxEvent(component, tool, name, value); diff --git a/Editor/Analytics/Helpers/GenericEditorMenuItemClickEventSenderHelpers.cs b/Editor/Analytics/Helpers/GenericEditorMenuItemClickEventSenderHelpers.cs index 4751724..f630eff 100644 --- a/Editor/Analytics/Helpers/GenericEditorMenuItemClickEventSenderHelpers.cs +++ b/Editor/Analytics/Helpers/GenericEditorMenuItemClickEventSenderHelpers.cs @@ -3,11 +3,21 @@ namespace UnityEditor.Purchasing internal static class GenericEditorMenuItemClickEventSenderHelpers { internal static void SendGameObjectMenuAddIapButtonEvent() + { + BuildAndSendEvent(EventComponents.k_ComponentCodeless, EventSourceMenuItems.k_SourceGameObjectMenu, EventActions.k_ActionAddIapButton_legacy); + } + + internal static void SendGameObjectMenuAddCodelessIapButtonEvent() { BuildAndSendEvent(EventComponents.k_ComponentCodeless, EventSourceMenuItems.k_SourceGameObjectMenu, EventActions.k_ActionAddIapButton); } internal static void SendIapMenuAddIapButtonEvent() + { + BuildAndSendEvent(EventComponents.k_ComponentCodeless, EventSourceMenuItems.k_SourceIapMenu, EventActions.k_ActionAddIapButton_legacy); + } + + internal static void SendIapMenuAddCodelessIapButtonEvent() { BuildAndSendEvent(EventComponents.k_ComponentCodeless, EventSourceMenuItems.k_SourceIapMenu, EventActions.k_ActionAddIapButton); } diff --git a/Editor/Analytics/PurchasingServiceAnalyticsSender.cs b/Editor/Analytics/PurchasingServiceAnalyticsSender.cs index dad0bd5..fd7d409 100644 --- a/Editor/Analytics/PurchasingServiceAnalyticsSender.cs +++ b/Editor/Analytics/PurchasingServiceAnalyticsSender.cs @@ -5,7 +5,7 @@ namespace UnityEditor.Purchasing [InitializeOnLoad] internal static class PurchasingServiceAnalyticsSender { - static IAnalyticsPackageKeyHolder m_Holder; + static readonly IAnalyticsPackageKeyHolder m_Holder; static PurchasingServiceAnalyticsSender() { diff --git a/Editor/AppStoreExtensionMethods.cs b/Editor/AppStoreExtensionMethods.cs index 351efc8..4ea9f4d 100644 --- a/Editor/AppStoreExtensionMethods.cs +++ b/Editor/AppStoreExtensionMethods.cs @@ -35,8 +35,8 @@ public static AppStore ToAppStoreFromDisplayName(this string value) public static bool IsAndroid(this AppStore value) { - return (int) value >= (int) AppStoreMeta.AndroidStoreStart && - (int) value <= (int) AppStoreMeta.AndroidStoreEnd; + return (int)value >= (int)AppStoreMeta.AndroidStoreStart && + (int)value <= (int)AppStoreMeta.AndroidStoreEnd; } } } diff --git a/Editor/AppleCapabilities.cs b/Editor/AppleCapabilities.cs index 9207d73..208f213 100644 --- a/Editor/AppleCapabilities.cs +++ b/Editor/AppleCapabilities.cs @@ -14,7 +14,10 @@ class AppleCapabilities : IPostprocessBuildWithReport public void OnPostprocessBuild(BuildReport report) { - OnPostprocessBuild(report.summary.platform, report.summary.outputPath); + if (report.summary.platform == BuildTarget.tvOS || report.summary.platform == BuildTarget.iOS) + { + OnPostprocessBuild(report.summary.platform, report.summary.outputPath); + } } static void OnPostprocessBuild(BuildTarget buildTarget, string path) @@ -45,7 +48,7 @@ static void AddInAppPurchasingCapability(string projPath, PBXProject proj) static void AddStoreKitFramework(PBXProject proj, string projPath) { - foreach (var targetGuid in new [] {proj.GetUnityMainTargetGuid(), proj.GetUnityFrameworkTargetGuid()}) + foreach (var targetGuid in new[] { proj.GetUnityMainTargetGuid(), proj.GetUnityFrameworkTargetGuid() }) { proj.AddFrameworkToProject(targetGuid, k_StorekitFramework, false); System.IO.File.WriteAllText(projPath, proj.WriteToString()); diff --git a/Editor/ApplePriceTiers.cs b/Editor/ApplePriceTiers.cs index 0a956bb..3f6dbd8 100644 --- a/Editor/ApplePriceTiers.cs +++ b/Editor/ApplePriceTiers.cs @@ -1,76 +1,98 @@ -namespace UnityEditor.Purchasing +namespace UnityEditor.Purchasing { - internal static class ApplePriceTiers - { - internal const int kNumTiers = 88; + internal static class ApplePriceTiers + { + internal const int kNumTiers = 88; - // Cache - private static string [] s_Strings; - private static int [] s_Dollars; + // Cache + private static string[] s_Strings; + private static int[] s_Dollars; - internal static string [] Strings { - get { - GenerateAppleTierData (); - return s_Strings; - } - } + internal static string[] Strings + { + get + { + GenerateAppleTierData(); + return s_Strings; + } + } - internal static int [] RoundedDollars { - get { - GenerateAppleTierData (); - return s_Dollars; - } - } + internal static int[] RoundedDollars + { + get + { + GenerateAppleTierData(); + return s_Dollars; + } + } - internal static double ActualDollarsForAppleTier (int tier) - { - if (RoundedDollars[tier] == 0) - return 0; - - return RoundedDollars[tier] - 0.01; - } + internal static double ActualDollarsForAppleTier(int tier) + { + if (RoundedDollars[tier] == 0) + { + return 0; + } - private static void GenerateAppleTierData () - { - if (s_Strings == null || s_Dollars == null) { - s_Strings = new string [kNumTiers]; - s_Dollars = new int [kNumTiers]; + return RoundedDollars[tier] - 0.01; + } - var i = 0; - s_Dollars [i] = 0; - s_Strings [i++] = "Free"; + private static void GenerateAppleTierData() + { + if (s_Strings == null || s_Dollars == null) + { + s_Strings = new string[kNumTiers]; + s_Dollars = new int[kNumTiers]; - var dollars = 1; - for (; i < kNumTiers; ++i) { - if (i == 63) { - s_Strings [i] = CreateApplePriceTierString (i, 125); - s_Dollars [i] = 125; - } else if (i == 69) { - s_Strings [i] = CreateApplePriceTierString (i, 175); - s_Dollars [i] = 175; - } else { - s_Strings [i] = CreateApplePriceTierString (i, dollars); - s_Dollars [i] = dollars; + var i = 0; + s_Dollars[i] = 0; + s_Strings[i++] = "Free"; - if (i >= 82) { // 82 - 87 USD $100 increments to $1000 - dollars += 100; - } else if (i >= 77) { // 77 - 82 USD $50 increments to $500 - dollars += 50; - } else if (i >= 60) { // 60 - 77 $10 increments to $250, except 63 = $125 and 69 = $175 - dollars += 10; - } else if (i >= 50) { // 50 - 59 USD $5 increments - dollars += 5; - } else { // 1 - 49 USD $1 increments - dollars++; - } - } - } - } - } + var dollars = 1; + for (; i < kNumTiers; ++i) + { + if (i == 63) + { + s_Strings[i] = CreateApplePriceTierString(i, 125); + s_Dollars[i] = 125; + } + else if (i == 69) + { + s_Strings[i] = CreateApplePriceTierString(i, 175); + s_Dollars[i] = 175; + } + else + { + s_Strings[i] = CreateApplePriceTierString(i, dollars); + s_Dollars[i] = dollars; - private static string CreateApplePriceTierString (int tier, int roundedDollars) - { - return string.Format ("Tier {0} - USD {1:0.00}", tier, (float)roundedDollars - 0.01f); - } - } + if (i >= 82) + { // 82 - 87 USD $100 increments to $1000 + dollars += 100; + } + else if (i >= 77) + { // 77 - 82 USD $50 increments to $500 + dollars += 50; + } + else if (i >= 60) + { // 60 - 77 $10 increments to $250, except 63 = $125 and 69 = $175 + dollars += 10; + } + else if (i >= 50) + { // 50 - 59 USD $5 increments + dollars += 5; + } + else + { // 1 - 49 USD $1 increments + dollars++; + } + } + } + } + } + + private static string CreateApplePriceTierString(int tier, int roundedDollars) + { + return string.Format("Tier {0} - USD {1:0.00}", tier, roundedDollars - 0.01f); + } + } } diff --git a/Editor/AppleXMLProductCatalogExporter.cs b/Editor/AppleXMLProductCatalogExporter.cs index 632b844..20b2bce 100644 --- a/Editor/AppleXMLProductCatalogExporter.cs +++ b/Editor/AppleXMLProductCatalogExporter.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using System.IO; -using System.Text; using System.Collections.ObjectModel; +using System.IO; using System.Security.Cryptography; +using System.Text; using System.Xml.Linq; using UnityEngine.Purchasing; using ExporterValidationResults = UnityEditor.Purchasing.ProductCatalogEditor.ExporterValidationResults; @@ -17,50 +17,22 @@ internal class AppleXMLProductCatalogExporter : ProductCatalogEditor.IProductCat { internal static string kMandatoryExportFolder; - internal List kFilesToCopy = new List (); + internal List kFilesToCopy = new List(); private const string kNewLine = "\n"; - public string DisplayName { - get { - return "Apple XML Delivery"; - } - } + public string DisplayName => "Apple XML Delivery"; - public string DefaultFileName { - get { - return "metadata"; - } - } + public string DefaultFileName => "metadata"; - public string FileExtension { - get { - return "xml"; - } - } + public string FileExtension => "xml"; - public string StoreName { - get { - return AppleAppStore.Name; - } - } + public string StoreName => AppleAppStore.Name; - public string MandatoryExportFolder { - get { - return kMandatoryExportFolder; - } - } + public string MandatoryExportFolder => kMandatoryExportFolder; - public List FilesToCopy { - get { - return kFilesToCopy; - } - } + public List FilesToCopy => kFilesToCopy; - public bool SaveCompletePackage { - get { - return true; - } - } + public bool SaveCompletePackage => true; public string Export(ProductCatalog catalog) { @@ -69,42 +41,45 @@ public string Export(ProductCatalog catalog) // throughout this method, so this is converted to a list and then // wrapped in a ReadOnlyCollection to prevent mutation. var localesToExport = new ReadOnlyCollection(new List(GetLocalesToExport(catalog))); - XDeclaration declaration = new XDeclaration("1.0", "utf-8", "yes"); - XDocument document = new XDocument(); + var declaration = new XDeclaration("1.0", "utf-8", "yes"); + var document = new XDocument(); XNamespace ns = "http://apple.com/itunes/importer"; - XElement package = new XElement(ns + "package", + var package = new XElement(ns + "package", new XAttribute("version", "software5.7")); document.Add(package); package.Add(new XElement(ns + "provider", catalog.appleTeamID)); package.Add(new XElement(ns + "team_id", catalog.appleTeamID)); - XElement software = new XElement(ns + "software"); + var software = new XElement(ns + "software"); package.Add(software); software.Add(new XElement(ns + "vendor_id", catalog.appleSKU)); - XElement softwareMetadata = new XElement(ns + "software_metadata"); + var softwareMetadata = new XElement(ns + "software_metadata"); software.Add(softwareMetadata); - XElement inAppPurchases = new XElement(ns + "in_app_purchases"); + var inAppPurchases = new XElement(ns + "in_app_purchases"); softwareMetadata.Add(inAppPurchases); - foreach (var item in catalog.allProducts) { - XElement inAppPurchase = new XElement(ns + "in_app_purchase", + foreach (var item in catalog.allProducts) + { + var inAppPurchase = new XElement(ns + "in_app_purchase", new XElement(ns + "product_id", item.GetStoreID(AppleAppStore.Name) ?? item.id), new XElement(ns + "reference_name", item.id), new XElement(ns + "type", ProductTypeString(item))); - XElement products = new XElement(ns + "products"); + var products = new XElement(ns + "products"); inAppPurchase.Add(products); - XElement product = new XElement(ns + "product", + var product = new XElement(ns + "product", new XElement(ns + "cleared_for_sale", true), new XElement(ns + "wholesale_price_tier", item.applePriceTier)); products.Add(product); - XElement locales = new XElement(ns + "locales"); + var locales = new XElement(ns + "locales"); inAppPurchase.Add(locales); // Variable number of localizations, not every product will specify a localization for every language // so some of the these descriptions may be missing, in which case we just skip it. - foreach (var loc in localesToExport) { - LocalizedProductDescription desc = item.defaultDescription.googleLocale == loc ? item.defaultDescription : item.GetDescription(loc); - if (desc != null) { - XElement locale = new XElement(ns + "locale", + foreach (var loc in localesToExport) + { + var desc = item.defaultDescription.googleLocale == loc ? item.defaultDescription : item.GetDescription(loc); + if (desc != null) + { + var locale = new XElement(ns + "locale", new XAttribute("name", LocaleToAppleString(loc)), new XElement(ns + "title", desc.Title), new XElement(ns + "description", desc.Description)); @@ -112,11 +87,12 @@ public string Export(ProductCatalog catalog) } } - XElement reviewScreenshot = new XElement(ns + "review_screenshot"); + var reviewScreenshot = new XElement(ns + "review_screenshot"); inAppPurchase.Add(reviewScreenshot); reviewScreenshot.Add(new XElement(ns + "file_name", Path.GetFileName(item.screenshotPath))); - FileInfo fileInfo = new FileInfo(item.screenshotPath); - if (fileInfo.Exists) { + var fileInfo = new FileInfo(item.screenshotPath); + if (fileInfo.Exists) + { reviewScreenshot.Add(new XElement(ns + "size", fileInfo.Length)); reviewScreenshot.Add(new XElement(ns + "checksum", GetMD5Hash(fileInfo))); } @@ -132,14 +108,17 @@ public ExporterValidationResults Validate(ProductCatalog catalog) var results = new ExporterValidationResults(); // Warn if exporting an empty catalog - if (catalog.allProducts.Count == 0) { + if (catalog.allProducts.Count == 0) + { results.warnings.Add("Catalog is empty"); } // Check for duplicate IDs var usedIds = new HashSet(); - foreach (var product in catalog.allProducts) { - if (usedIds.Contains(product.id)) { + foreach (var product in catalog.allProducts) + { + if (usedIds.Contains(product.id)) + { results.errors.Add("More than one product uses the ID \"" + product.id + "\""); } usedIds.Add(product.id); @@ -147,10 +126,13 @@ public ExporterValidationResults Validate(ProductCatalog catalog) // Check for duplicate store IDs var usedStoreIds = new HashSet(); - foreach (var product in catalog.allProducts) { + foreach (var product in catalog.allProducts) + { var storeID = product.GetStoreID(AppleAppStore.Name); - if (!string.IsNullOrEmpty(storeID)) { - if (usedStoreIds.Contains(storeID)) { + if (!string.IsNullOrEmpty(storeID)) + { + if (usedStoreIds.Contains(storeID)) + { results.errors.Add("More than one product uses the Apple store ID \"" + storeID + "\""); } usedIds.Add(product.id); @@ -160,23 +142,29 @@ public ExporterValidationResults Validate(ProductCatalog catalog) // Check for duplicate runtime IDs -- this conflict could occur if a product has a base ID that is the // same as another product's store-specific ID var runtimeIDs = new HashSet(); - foreach (var product in catalog.allProducts) { + foreach (var product in catalog.allProducts) + { var runtimeID = product.GetStoreID(AppleAppStore.Name) ?? product.id; - if (runtimeIDs.Contains(runtimeID)) { + if (runtimeIDs.Contains(runtimeID)) + { results.errors.Add("More than one product is identified by the ID \"" + runtimeID + "\""); } runtimeIDs.Add(runtimeID); } // Check SKU - if (string.IsNullOrEmpty(catalog.appleSKU)) { + if (string.IsNullOrEmpty(catalog.appleSKU)) + { results.fieldErrors["appleSKU"] = "Apple SKU is required. Find this in iTunesConnect."; - } else { + } + else + { kMandatoryExportFolder = catalog.appleSKU + ".itmsp"; } // Check Team ID - if (string.IsNullOrEmpty(catalog.appleTeamID)) { + if (string.IsNullOrEmpty(catalog.appleTeamID)) + { results.fieldErrors["appleTeamID"] = "Apple Team ID is required. Find this on https://developer.apple.com."; } @@ -188,24 +176,30 @@ public ExporterValidationResults Validate(ProductCatalogItem item) var results = new ExporterValidationResults(); // Check for missing IDs - if (string.IsNullOrEmpty(item.id)) { + if (string.IsNullOrEmpty(item.id)) + { results.fieldErrors["id"] = "ID is required"; } // Check for missing title - if (string.IsNullOrEmpty(item.defaultDescription.Title)) { + if (string.IsNullOrEmpty(item.defaultDescription.Title)) + { results.fieldErrors["defaultDescription.Title"] = "Title is required"; } // Check for missing description - if (string.IsNullOrEmpty(item.defaultDescription.Description)) { + if (string.IsNullOrEmpty(item.defaultDescription.Description)) + { results.fieldErrors["defaultDescription.Description"] = "Description is required"; } // Check for screenshot - if (string.IsNullOrEmpty(item.screenshotPath)) { + if (string.IsNullOrEmpty(item.screenshotPath)) + { results.fieldErrors["screenshotPath"] = "Screenshot is required"; - } else { + } + else + { kFilesToCopy.Add(item.screenshotPath); } @@ -216,13 +210,19 @@ private HashSet GetLocalesToExport(ProductCatalog catalog) { var locs = new HashSet(); - foreach (var item in catalog.allProducts) { + foreach (var item in catalog.allProducts) + { if (item.defaultDescription.googleLocale.SupportedOnApple()) + { locs.Add(item.defaultDescription.googleLocale); + } - foreach (var desc in item.translatedDescriptions) { + foreach (var desc in item.translatedDescriptions) + { if (desc.googleLocale.SupportedOnApple()) + { locs.Add(desc.googleLocale); + } } } @@ -231,13 +231,14 @@ private HashSet GetLocalesToExport(ProductCatalog catalog) private static string ProductTypeString(ProductCatalogItem item) { - switch (item.type) { - case ProductType.Consumable: - return "consumable"; - case ProductType.NonConsumable: - return "non-consumable"; - case ProductType.Subscription: - return "subscription"; + switch (item.type) + { + case ProductType.Consumable: + return "consumable"; + case ProductType.NonConsumable: + return "non-consumable"; + case ProductType.Subscription: + return "subscription"; } return string.Empty; @@ -245,7 +246,8 @@ private static string ProductTypeString(ProductCatalogItem item) private static string LocaleToAppleString(TranslationLocale loc) { - switch (loc) { + switch (loc) + { // Apple uses Hans and Hant, rather than Cn and TW case TranslationLocale.zh_CN: return "zh-Hans"; @@ -281,18 +283,18 @@ public ProductCatalog NormalizeToType(ProductCatalog catalog) public static string GetMD5Hash(FileInfo fileInfo) { - MD5 md5 = MD5.Create(); - FileStream fileStream = fileInfo.OpenRead(); + var md5 = MD5.Create(); + var fileStream = fileInfo.OpenRead(); // Convert the input string to a byte array and compute the hash. - byte[] data = md5.ComputeHash(fileStream); + var data = md5.ComputeHash(fileStream); // Create a new StringBuilder to collect the bytes // and create a string. - StringBuilder stringBuilder = new StringBuilder(); + var stringBuilder = new StringBuilder(); - // Loop through each byte of the hashed data + // Loop through each byte of the hashed data // and format each one as a hexadecimal string. - for (int i = 0; i < data.Length; i++) + for (var i = 0; i < data.Length; i++) { stringBuilder.Append(data[i].ToString("x2")); } diff --git a/Editor/AssemblyInfo.cs b/Editor/AssemblyInfo.cs index f893aa1..f389311 100644 --- a/Editor/AssemblyInfo.cs +++ b/Editor/AssemblyInfo.cs @@ -1,8 +1,7 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.CompilerServices; -[assembly:InternalsVisibleTo("UnityEditor.Purchasing.EditorTests")] -[assembly:InternalsVisibleTo("Unity.IntegrationTests")] -[assembly:InternalsVisibleTo("Unity.RuntimeTests")] -[assembly:InternalsVisibleTo("DynamicProxyGenAssembly2")] - +[assembly: InternalsVisibleTo("UnityEditor.Purchasing.EditorTests")] +[assembly: InternalsVisibleTo("Unity.IntegrationTests")] +[assembly: InternalsVisibleTo("Unity.RuntimeTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/Editor/BuildTargetGroupExtensions.cs b/Editor/BuildTargetGroupExtensions.cs index 4e442e5..80739c8 100644 --- a/Editor/BuildTargetGroupExtensions.cs +++ b/Editor/BuildTargetGroupExtensions.cs @@ -31,23 +31,23 @@ internal static ReadOnlyCollection ToAppStores(this BuildTargetGroup v case BuildTargetGroup.iOS: case BuildTargetGroup.tvOS: - storesArray = new[] {AppStore.AppleAppStore}; + storesArray = new[] { AppStore.AppleAppStore }; break; case BuildTargetGroup.WSA: - storesArray = new[] {AppStore.WinRT}; + storesArray = new[] { AppStore.WinRT }; break; case BuildTargetGroup.Standalone: if (Application.platform == RuntimePlatform.OSXEditor) { - storesArray = new[] {AppStore.MacAppStore}; + storesArray = new[] { AppStore.MacAppStore }; break; } goto default; default: - storesArray = new[] {AppStore.fake}; + storesArray = new[] { AppStore.fake }; break; } diff --git a/Editor/CodelessIAPButtonEditor.cs b/Editor/CodelessIAPButtonEditor.cs new file mode 100644 index 0000000..26efe75 --- /dev/null +++ b/Editor/CodelessIAPButtonEditor.cs @@ -0,0 +1,28 @@ +using UnityEngine.Purchasing; + +namespace UnityEditor.Purchasing +{ + /// + /// Customer Editor class for the CodelessIAPButton. This class handle how the CodelessIAPButton should represent itself in the UnityEditor. + /// + [CustomEditor(typeof(CodelessIAPButton))] + [CanEditMultipleObjects] + public class CodelessIAPButtonEditor : AbstractIAPButtonEditor + { + /// + /// Event trigger when CodelessIAPButton is enabled in the scene. + /// + public void OnEnable() + { + OnEnableInternal(); + } + + /// + /// Event trigger when trying to draw the CodelessIAPButton in the inspector. + /// + public override void OnInspectorGUI() + { + OnInspectorGuiInternal(); + } + } +} diff --git a/Editor/CodelessIAPButtonEditor.cs.meta b/Editor/CodelessIAPButtonEditor.cs.meta new file mode 100644 index 0000000..3e2d5fb --- /dev/null +++ b/Editor/CodelessIAPButtonEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 09c1cfc6428747bc82020d7e1f9d1292 +timeCreated: 1678482150 \ No newline at end of file diff --git a/Editor/GooglePlayProductCatalogExporter.cs b/Editor/GooglePlayProductCatalogExporter.cs index 7686954..675853a 100644 --- a/Editor/GooglePlayProductCatalogExporter.cs +++ b/Editor/GooglePlayProductCatalogExporter.cs @@ -1,283 +1,293 @@ -using System.Text; -using System.Collections.Generic; using System; +using System.Collections.Generic; +using System.Text; using UnityEngine.Purchasing; using ExporterValidationResults = UnityEditor.Purchasing.ProductCatalogEditor.ExporterValidationResults; namespace UnityEditor.Purchasing { /// - /// Exports a ProductCatalog to the CSV format expected by Google Play's batch import tools. - /// - internal class GooglePlayProductCatalogExporter : ProductCatalogEditor.IProductCatalogExporter - { - public string DisplayName { - get { - return "Google Play CSV"; - } - } - - public string DefaultFileName { - get { - return "GooglePlayProductCatalog"; - } - } - - public string FileExtension { - get { - return "csv"; - } - } - - public string StoreName { - get { - return GooglePlay.Name; - } - } - - public string MandatoryExportFolder { - get { - return null; - } - } - - public List FilesToCopy { - get { - return null; + /// Exports a ProductCatalog to the CSV format expected by Google Play's batch import tools. + /// + internal class GooglePlayProductCatalogExporter : ProductCatalogEditor.IProductCatalogExporter + { + public string DisplayName => "Google Play CSV"; + + public string DefaultFileName => "GooglePlayProductCatalog"; + + public string FileExtension => "csv"; + + public string StoreName => GooglePlay.Name; + + public string MandatoryExportFolder => null; + + public List FilesToCopy => null; + + public bool SaveCompletePackage => false; + + public string Export(ProductCatalog catalog) + { + var fileContents = new StringBuilder(); + var values = new string[8]; + + // Write column headers + values[0] = "Product ID"; + values[1] = "Published State"; + values[2] = "Purchase Type"; + values[3] = "Auto Translate"; + values[4] = "Locale; Title; Description"; + values[5] = "Auto Fill Prices"; + values[6] = "Price"; + values[7] = "Pricing Template ID"; + fileContents.Append(string.Join(kComma, values)); + fileContents.Append("\n"); + + foreach (var product in catalog.allProducts) + { + values[0] = string.IsNullOrEmpty(product.GetStoreID(GooglePlay.Name)) + ? CSVEscape(product.id) + : CSVEscape(product.GetStoreID(GooglePlay.Name)); + + values[1] = "published"; + values[2] = ProductTypeString(product.type); + + values[3] = kFalse; + values[4] = PackTitlesAndDescriptions(product); + + if (string.IsNullOrEmpty(product.pricingTemplateID)) + { + values[5] = kTrue; + values[6] = PackPrice(product); + values[7] = string.Empty; + } + else + { + values[5] = kFalse; + values[6] = string.Empty; + values[7] = product.pricingTemplateID; + } + + fileContents.Append(string.Join(kComma, values)); + fileContents.Append("\n"); } + + return fileContents.ToString(); } - public bool SaveCompletePackage { - get { - return false; + public ExporterValidationResults Validate(ProductCatalog catalog) + { + var results = new ExporterValidationResults(); + + // Warn if exporting an empty catalog + if (catalog.allProducts.Count == 0) + { + results.warnings.Add("Catalog is empty"); + } + + // Check for duplicate IDs + var usedIDs = new HashSet(); + foreach (var product in catalog.allProducts) + { + if (usedIDs.Contains(product.id)) + { + results.errors.Add("More than one product uses the ID \"" + product.id + "\""); + } + usedIDs.Add(product.id); + } + + // Check for duplicate store IDs + var usedStoreIDs = new HashSet(); + foreach (var product in catalog.allProducts) + { + var storeID = product.GetStoreID(GooglePlay.Name); + if (!string.IsNullOrEmpty(storeID)) + { + if (usedStoreIDs.Contains(storeID)) + { + results.errors.Add("More than one product uses the Google Play store ID \"" + storeID + "\""); + } + usedStoreIDs.Add(product.id); + } + } + + // Check for duplicate runtime IDs -- this conflict could occur if a product has a base ID that is the + // same as another product's store-specific ID + var runtimeIDs = new HashSet(); + foreach (var product in catalog.allProducts) + { + var runtimeID = product.GetStoreID(GooglePlay.Name); + if (string.IsNullOrEmpty(runtimeID)) + { + runtimeID = product.id; + } + + if (runtimeIDs.Contains(runtimeID)) + { + results.errors.Add("More than one product is identified by the ID \"" + runtimeID + "\""); + } + runtimeIDs.Add(runtimeID); } + + return results; } - public string Export(ProductCatalog catalog) - { - var fileContents = new StringBuilder(); - var values = new string[8]; - - // Write column headers - values[0] = "Product ID"; - values[1] = "Published State"; - values[2] = "Purchase Type"; - values[3] = "Auto Translate"; - values[4] = "Locale; Title; Description"; - values[5] = "Auto Fill Prices"; - values[6] = "Price"; - values[7] = "Pricing Template ID"; - fileContents.Append(string.Join(kComma, values)); - fileContents.Append("\n"); - - foreach (var product in catalog.allProducts) { - if (string.IsNullOrEmpty(product.GetStoreID(GooglePlay.Name))) { - values[0] = CSVEscape(product.id); - } else { - values[0] = CSVEscape(product.GetStoreID(GooglePlay.Name)); - } - - values[1] = "published"; - values[2] = ProductTypeString(product.type); - - values[3] = kFalse; - values[4] = PackTitlesAndDescriptions(product); - - if (string.IsNullOrEmpty(product.pricingTemplateID)) { - values[5] = kTrue; - values[6] = PackPrice(product); - values[7] = string.Empty; - } else { - values[5] = kFalse; - values[6] = string.Empty; - values[7] = product.pricingTemplateID; - } - - fileContents.Append(string.Join(kComma, values)); - fileContents.Append("\n"); - } - - return fileContents.ToString(); - } - - public ExporterValidationResults Validate(ProductCatalog catalog) - { - var results = new ExporterValidationResults(); - - // Warn if exporting an empty catalog - if (catalog.allProducts.Count == 0) { - results.warnings.Add("Catalog is empty"); - } - - // Check for duplicate IDs - var usedIDs = new HashSet(); - foreach (var product in catalog.allProducts) { - if (usedIDs.Contains(product.id)) { - results.errors.Add("More than one product uses the ID \"" + product.id + "\""); - } - usedIDs.Add(product.id); - } - - // Check for duplicate store IDs - var usedStoreIDs = new HashSet(); - foreach (var product in catalog.allProducts) { - var storeID = product.GetStoreID(GooglePlay.Name); - if (!string.IsNullOrEmpty(storeID)) { - if (usedStoreIDs.Contains(storeID)) { - results.errors.Add("More than one product uses the Google Play store ID \"" + storeID + "\""); - } - usedStoreIDs.Add(product.id); - } - } - - // Check for duplicate runtime IDs -- this conflict could occur if a product has a base ID that is the - // same as another product's store-specific ID - var runtimeIDs = new HashSet(); - foreach (var product in catalog.allProducts) { - var runtimeID = product.GetStoreID(GooglePlay.Name); - if (string.IsNullOrEmpty(runtimeID)) { - runtimeID = product.id; - } - - if (runtimeIDs.Contains(runtimeID)) { - results.errors.Add("More than one product is identified by the ID \"" + runtimeID + "\""); - } - runtimeIDs.Add(runtimeID); - } - - return results; - } - - public ExporterValidationResults Validate(ProductCatalogItem item) - { - var results = new ExporterValidationResults(); - - // Check for missing IDs - if (string.IsNullOrEmpty(item.id)) { - results.errors.Add("ID is required"); - } - - // A product ID must start with a lowercase letter or a number and must be composed - // of only lowercase letters (a-z), numbers (0-9), underscores (_), and periods (.) - string actualID = item.GetStoreID(GooglePlay.Name) ?? item.id; - string field = (actualID == item.GetStoreID(GooglePlay.Name)) ? "storeID." + GooglePlay.Name : "id"; - if (Char.IsNumber(actualID[0]) || (Char.IsLower(actualID[0]) && Char.IsLetter(actualID[0]))) { - foreach (char c in actualID) { - if (c != '_' && c != '.' && !Char.IsNumber(c) && !(Char.IsLetter(c) && Char.IsLower(c))) { - results.fieldErrors[field] = "Product ID \"" + actualID + "\" must contain only lowercase letters, numbers, underscores, and periods"; - } - } - } else { - results.fieldErrors[field] = "Product ID \"" + actualID + "\" must start with a lowercase letter or a number"; - } - - ValidateDescription(item.defaultDescription, ref results, "defaultDescription"); - foreach (var desc in item.translatedDescriptions) { - ValidateDescription(desc, ref results); - } - - // Check for missing price information - if (string.IsNullOrEmpty(item.pricingTemplateID) && item.googlePrice.value == 0) { - results.fieldErrors["googlePrice"] = "Items must have either a price or a pricing template ID"; - } - - return results; - } - - private void ValidateDescription(LocalizedProductDescription desc, ref ExporterValidationResults results, string fieldPrefix = null) - { - if (fieldPrefix == null) { - fieldPrefix = "translatedDescriptions." + desc.googleLocale.ToString(); - } - - // Check for missing title - if (string.IsNullOrEmpty(desc.Title)) { - results.fieldErrors[fieldPrefix + ".Title"] = "Title is required (" + desc.googleLocale.ToString() + ")"; - } else { - if (desc.Title.Length > 55) { // Titles can be up to 55 characters in length - results.fieldErrors[fieldPrefix + ".Title"] = "Title must not be longer than 55 characters (" + desc.googleLocale.ToString() + ")"; - } else if (desc.Title.Length > 25) { // Titles should be no longer than 25 characters - results.warnings.Add("Title should not be longer than 25 characters (" + desc.googleLocale.ToString() + ")"); - } - } - - // Check for missing description - if (string.IsNullOrEmpty(desc.Description)) { - results.fieldErrors[fieldPrefix + ".Description"] = "Description is required (" + desc.googleLocale.ToString() + ")"; - } else { - if (desc.Description.Length > 80) { // Descriptions can be up to 80 characters in length - results.fieldErrors[fieldPrefix + ".Description"] = "Description must not be longer than 80 characters (" + desc.googleLocale.ToString() + ")"; - } - } - } - - private const string kTrue = "true"; - private const string kFalse = "false"; - private const string kComma = ","; - private const string kSemicolon = ";"; - private const string kBackslash = "\\"; - private const string kQuote = "\""; - private const string kEscapedQuote = "\"\""; - private static char[] kCSVCharactersToQuote = { ',', '"', '\n' }; - - private static string CSVEscape(string s) - { - if (s == null) - return s; - - if (s.Contains(kQuote)) { - s = s.Replace(kQuote, kEscapedQuote); - } - - if (s.IndexOfAny(kCSVCharactersToQuote) > -1) { - s = kQuote + s + kQuote; - } - - return s; - } - - private static string SSVEscape(string s) - { - if (s == null) - return s; - - s.Replace(kBackslash, kBackslash + kBackslash); - s.Replace(kSemicolon, kBackslash + kSemicolon); - return s; - } - - private static string ProductTypeString(ProductType type) - { - return "managed_by_android"; - } - - private static string PackTitlesAndDescriptions(ProductCatalogItem product) - { - var values = new List(); - - values.Add(product.defaultDescription.googleLocale.ToString()); - values.Add(SSVEscape(product.defaultDescription.Title)); - values.Add(SSVEscape(product.defaultDescription.Description)); - - foreach (var desc in product.translatedDescriptions) { - values.Add(desc.googleLocale.ToString()); - values.Add(SSVEscape(desc.Title)); - values.Add(SSVEscape(desc.Description)); - } - - return CSVEscape(string.Join(kSemicolon, values.ToArray())); - } - - private const int kPriceMicroUnitMultiplier = 1000000; - private static string PackPrice(ProductCatalogItem product) - { - return CSVEscape(Convert.ToInt32(product.googlePrice.value * kPriceMicroUnitMultiplier).ToString()); - } - - public ProductCatalog NormalizeToType(ProductCatalog catalog) - { - return catalog; - } - } + public ExporterValidationResults Validate(ProductCatalogItem item) + { + var results = new ExporterValidationResults(); + + // Check for missing IDs + if (string.IsNullOrEmpty(item.id)) + { + results.errors.Add("ID is required"); + } + + // A product ID must start with a lowercase letter or a number and must be composed + // of only lowercase letters (a-z), numbers (0-9), underscores (_), and periods (.) + var actualID = item.GetStoreID(GooglePlay.Name) ?? item.id; + var field = (actualID == item.GetStoreID(GooglePlay.Name)) ? "storeID." + GooglePlay.Name : "id"; + if (Char.IsNumber(actualID[0]) || (Char.IsLower(actualID[0]) && Char.IsLetter(actualID[0]))) + { + foreach (var c in actualID) + { + if (c != '_' && c != '.' && !Char.IsNumber(c) && !(Char.IsLetter(c) && Char.IsLower(c))) + { + results.fieldErrors[field] = "Product ID \"" + actualID + "\" must contain only lowercase letters, numbers, underscores, and periods"; + } + } + } + else + { + results.fieldErrors[field] = "Product ID \"" + actualID + "\" must start with a lowercase letter or a number"; + } + + ValidateDescription(item.defaultDescription, ref results, "defaultDescription"); + foreach (var desc in item.translatedDescriptions) + { + ValidateDescription(desc, ref results); + } + + // Check for missing price information + if (string.IsNullOrEmpty(item.pricingTemplateID) && item.googlePrice.value == 0) + { + results.fieldErrors["googlePrice"] = "Items must have either a price or a pricing template ID"; + } + + return results; + } + + private void ValidateDescription(LocalizedProductDescription desc, ref ExporterValidationResults results, string fieldPrefix = null) + { + if (fieldPrefix == null) + { + fieldPrefix = "translatedDescriptions." + desc.googleLocale.ToString(); + } + + // Check for missing title + if (string.IsNullOrEmpty(desc.Title)) + { + results.fieldErrors[fieldPrefix + ".Title"] = "Title is required (" + desc.googleLocale.ToString() + ")"; + } + else + { + if (desc.Title.Length > 55) + { // Titles can be up to 55 characters in length + results.fieldErrors[fieldPrefix + ".Title"] = "Title must not be longer than 55 characters (" + desc.googleLocale.ToString() + ")"; + } + else if (desc.Title.Length > 25) + { // Titles should be no longer than 25 characters + results.warnings.Add("Title should not be longer than 25 characters (" + desc.googleLocale.ToString() + ")"); + } + } + + // Check for missing description + if (string.IsNullOrEmpty(desc.Description)) + { + results.fieldErrors[fieldPrefix + ".Description"] = "Description is required (" + desc.googleLocale.ToString() + ")"; + } + else + { + if (desc.Description.Length > 80) + { // Descriptions can be up to 80 characters in length + results.fieldErrors[fieldPrefix + ".Description"] = "Description must not be longer than 80 characters (" + desc.googleLocale.ToString() + ")"; + } + } + } + + private const string kTrue = "true"; + private const string kFalse = "false"; + private const string kComma = ","; + private const string kSemicolon = ";"; + private const string kBackslash = "\\"; + private const string kQuote = "\""; + private const string kEscapedQuote = "\"\""; + private static readonly char[] kCSVCharactersToQuote = { ',', '"', '\n' }; + + private static string CSVEscape(string s) + { + if (s == null) + { + return s; + } + + if (s.Contains(kQuote)) + { + s = s.Replace(kQuote, kEscapedQuote); + } + + if (s.IndexOfAny(kCSVCharactersToQuote) > -1) + { + s = kQuote + s + kQuote; + } + + return s; + } + + private static string SSVEscape(string s) + { + if (s == null) + { + return s; + } + + s.Replace(kBackslash, kBackslash + kBackslash); + s.Replace(kSemicolon, kBackslash + kSemicolon); + return s; + } + + private static string ProductTypeString(ProductType type) + { + return "managed_by_android"; + } + + private static string PackTitlesAndDescriptions(ProductCatalogItem product) + { + var values = new List + { + product.defaultDescription.googleLocale.ToString(), + SSVEscape(product.defaultDescription.Title), + SSVEscape(product.defaultDescription.Description) + }; + + foreach (var desc in product.translatedDescriptions) + { + values.Add(desc.googleLocale.ToString()); + values.Add(SSVEscape(desc.Title)); + values.Add(SSVEscape(desc.Description)); + } + + return CSVEscape(string.Join(kSemicolon, values.ToArray())); + } + + private const int kPriceMicroUnitMultiplier = 1000000; + private static string PackPrice(ProductCatalogItem product) + { + return CSVEscape(Convert.ToInt32(product.googlePrice.value * kPriceMicroUnitMultiplier).ToString()); + } + + public ProductCatalog NormalizeToType(ProductCatalog catalog) + { + return catalog; + } + } } diff --git a/Editor/IAPButtonEditor.cs b/Editor/IAPButtonEditor.cs index 7146c56..667afc7 100644 --- a/Editor/IAPButtonEditor.cs +++ b/Editor/IAPButtonEditor.cs @@ -1,68 +1,31 @@ using UnityEngine; using UnityEngine.Purchasing; -using System.Collections.Generic; -using static UnityEditor.Purchasing.UnityPurchasingEditor; namespace UnityEditor.Purchasing { /// /// Customer Editor class for the IAPButton. This class handle how the IAPButton should represent itself in the UnityEditor. /// - [CustomEditor(typeof(IAPButton))] - [CanEditMultipleObjects] - public class IAPButtonEditor : Editor - { - private static readonly string[] excludedFields = new string[] { "m_Script" }; - private static readonly string[] restoreButtonExcludedFields = new string[] { "m_Script", "consumePurchase", "onPurchaseComplete", "onPurchaseFailed", "titleText", "descriptionText", "priceText" }; - private const string kNoProduct = ""; - - private List m_ValidIDs = new List(); - private SerializedProperty m_ProductIDProperty; - - /// - /// Event trigger when IAPButton is enabled in the scene. - /// +//disable Warning CS0618 IAPButton is deprecated, please use CodelessIAPButton instead. +#pragma warning disable 0618 + [CustomEditor(typeof(IAPButton))] + [CanEditMultipleObjects] + public class IAPButtonEditor : AbstractIAPButtonEditor + { + /// + /// Event trigger when IAPButton is enabled in the scene. + /// public void OnEnable() - { - m_ProductIDProperty = serializedObject.FindProperty("productId"); - } + { + OnEnableInternal(); + } - /// - /// Event trigger when trying to draw the IAPButton in the inspector. - /// + /// + /// Event trigger when trying to draw the IAPButton in the inspector. + /// public override void OnInspectorGUI() - { - IAPButton button = (IAPButton)target; - - serializedObject.Update(); - - if (button.buttonType == IAPButton.ButtonType.Purchase) { - EditorGUILayout.LabelField(new GUIContent("Product ID:", "Select a product from the IAP catalog.")); - - var catalog = ProductCatalog.LoadDefaultCatalog(); - - m_ValidIDs.Clear(); - m_ValidIDs.Add(kNoProduct); - foreach (var product in catalog.allProducts) { - m_ValidIDs.Add(product.id); - } - - int currentIndex = string.IsNullOrEmpty(button.productId) ? 0 : m_ValidIDs.IndexOf(button.productId); - int newIndex = EditorGUILayout.Popup(currentIndex, m_ValidIDs.ToArray()); - if (newIndex > 0 && newIndex < m_ValidIDs.Count) { - m_ProductIDProperty.stringValue = m_ValidIDs[newIndex]; - } else { - m_ProductIDProperty.stringValue = string.Empty; - } - - if (GUILayout.Button("IAP Catalog...")) { - ProductCatalogEditor.ShowWindow(); - } - } - - DrawPropertiesExcluding(serializedObject, button.buttonType == IAPButton.ButtonType.Restore ? restoreButtonExcludedFields : excludedFields); - - serializedObject.ApplyModifiedProperties(); - } - } + { + OnInspectorGuiInternal(); + } + } } diff --git a/Editor/Importer.cs b/Editor/Importer.cs index 123af18..f48ab90 100644 --- a/Editor/Importer.cs +++ b/Editor/Importer.cs @@ -1,6 +1,6 @@ -using UnityEditor.Purchasing; -using UnityEditor.Build; using System; +using UnityEditor.Build; +using UnityEditor.Purchasing; namespace UnityEditor { diff --git a/Editor/MenuItems/IapButtonMenu.cs b/Editor/MenuItems/IapButtonMenu.cs index 75ca7bb..1f8c37c 100644 --- a/Editor/MenuItems/IapButtonMenu.cs +++ b/Editor/MenuItems/IapButtonMenu.cs @@ -1,5 +1,7 @@ +using System; using UnityEngine; using UnityEngine.Purchasing; +using UnityEngine.UI; namespace UnityEditor.Purchasing { @@ -11,33 +13,58 @@ public static class IAPButtonMenu /// /// Add option to create a IAPButton from the GameObject menu. /// - [MenuItem("GameObject/" + IapMenuConsts.PurchasingDisplayName + "/IAP Button", false, 10)] + [MenuItem("GameObject/" + IapMenuConsts.PurchasingDisplayName + "/IAP Button (Legacy)", false, 11)] public static void GameObjectCreateUnityIAPButton() { - CreateUnityIAPButtonInternal(); + CreateUnityIAPButtonInternal("IAP Button (Legacy)"); GenericEditorMenuItemClickEventSenderHelpers.SendGameObjectMenuAddIapButtonEvent(); } + /// + /// Add option to create a CodelessIAPButton from the GameObject menu. + /// + [MenuItem("GameObject/" + IapMenuConsts.PurchasingDisplayName + "/IAP Button", false, 10)] + public static void GameObjectCreateUnityCodelessIAPButton() + { + CreateUnityCodelessIAPButtonInternal("IAP Button"); + + GenericEditorMenuItemClickEventSenderHelpers.SendGameObjectMenuAddCodelessIapButtonEvent(); + } + /// /// Add option to create a IAPButton from the Window/UnityIAP menu. /// - [MenuItem (IapMenuConsts.MenuItemRoot + "/Create IAP Button", false, 100)] + [MenuItem(IapMenuConsts.MenuItemRoot + "/Create IAP Button (Legacy)", false, 101)] public static void CreateUnityIAPButton() { - CreateUnityIAPButtonInternal(); + CreateUnityIAPButtonInternal("IAP Button (Legacy)"); GenericEditorMenuItemClickEventSenderHelpers.SendIapMenuAddIapButtonEvent(); GameServicesEventSenderHelpers.SendTopMenuCreateIapButtonEvent(); } - static void CreateUnityIAPButtonInternal() + /// + /// Add option to create a CodelessIAPButton from the Window/UnityIAP menu. + /// + [MenuItem(IapMenuConsts.MenuItemRoot + "/Create IAP Button", false, 100)] + public static void CreateUnityCodelessIAPButton() { - GameObject buttonObject = CreateButtonObject(); + CreateUnityCodelessIAPButtonInternal("IAP Button"); + + GenericEditorMenuItemClickEventSenderHelpers.SendIapMenuAddCodelessIapButtonEvent(); + GameServicesEventSenderHelpers.SendTopMenuCreateCodelessIapButtonEvent(); + } + + static void CreateUnityIAPButtonInternal(string name) + { + var buttonObject = ItemCreationUtility.CreateGameObject(name, typeof(Button)); if (buttonObject) { - IAPButton iapButton = buttonObject.AddComponent(); + //disable Warning CS0618 IAPButton is deprecated, please use CodelessIAPButton instead. +#pragma warning disable 0618 + var iapButton = buttonObject.AddComponent(); if (iapButton != null) { @@ -48,20 +75,14 @@ static void CreateUnityIAPButtonInternal() } } - static GameObject CreateButtonObject() + static void CreateUnityCodelessIAPButtonInternal(string name) { - ExecuteButtonMenuItem(); + var emptyObject = ItemCreationUtility.CreateGameObject(name); - return Selection.activeGameObject; - } - - static void ExecuteButtonMenuItem() - { -#if UNITY_2022_1_OR_NEWER || (UNITY_2021_2_OR_NEWER && !(UNITY_2021_2_2 || UNITY_2021_2_1)) - EditorApplication.ExecuteMenuItem("GameObject/UI/Legacy/Button"); -#else - EditorApplication.ExecuteMenuItem("GameObject/UI/Button"); -#endif + if (emptyObject) + { + emptyObject.AddComponent(); + } } } } diff --git a/Editor/MenuItems/IapListenerMenu.cs b/Editor/MenuItems/IapListenerMenu.cs index b7d47af..2d17f34 100644 --- a/Editor/MenuItems/IapListenerMenu.cs +++ b/Editor/MenuItems/IapListenerMenu.cs @@ -23,7 +23,7 @@ public static void GameObjectCreateUnityIAPListener() /// /// Add option to create a IAPListener from the Window/UnityIAP menu. /// - [MenuItem (IapMenuConsts.MenuItemRoot + "/Create IAP Listener", false, 100)] + [MenuItem(IapMenuConsts.MenuItemRoot + "/Create IAP Listener", false, 100)] public static void CreateUnityIAPListener() { CreateUnityIAPListenerInternal(); @@ -34,9 +34,10 @@ public static void CreateUnityIAPListener() static void CreateUnityIAPListenerInternal() { - GameObject listenerObject = CreateListenerObject(); + var listenerObject = CreateListenerObject(); - if (listenerObject) { + if (listenerObject) + { listenerObject.AddComponent(); listenerObject.name = "IAP Listener"; } diff --git a/Editor/MenuItems/ItemCreationUtility.cs b/Editor/MenuItems/ItemCreationUtility.cs new file mode 100644 index 0000000..5698be0 --- /dev/null +++ b/Editor/MenuItems/ItemCreationUtility.cs @@ -0,0 +1,42 @@ +using System; +using UnityEngine; + +namespace UnityEditor.Purchasing +{ + /// + /// This code is taken from the com.unity.2d.sprite@1.0.0 package + /// + static class ItemCreationUtility + { + internal static GameObject CreateGameObject(string name) + { + var parent = Selection.activeGameObject; + var newGameObject = ObjectFactory.CreateGameObject(name); + CreateGameObject(name, newGameObject, parent); + return newGameObject; + } + + internal static GameObject CreateGameObject(string name, params Type[] components) + { + var parent = Selection.activeGameObject; + var newGameObject = ObjectFactory.CreateGameObject(name, components); + CreateGameObject(name, newGameObject, parent); + return newGameObject; + } + + static void CreateGameObject(string name, GameObject newGameObject, GameObject parent) + { + newGameObject.name = name; + Selection.activeObject = newGameObject; + GOCreationCommands.Place(newGameObject, parent); + if (EditorSettings.defaultBehaviorMode == EditorBehaviorMode.Mode2D) + { + var position = newGameObject.transform.position; + position.z = 0; + newGameObject.transform.position = position; + } + + Undo.RegisterCreatedObjectUndo(newGameObject, string.Format("Create {0}", name)); + } + } +} diff --git a/Editor/MenuItems/ItemCreationUtility.cs.meta b/Editor/MenuItems/ItemCreationUtility.cs.meta new file mode 100644 index 0000000..70c4d83 --- /dev/null +++ b/Editor/MenuItems/ItemCreationUtility.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b47b5eb4f2d34f4c9989c5b7377763d4 +timeCreated: 1678221166 \ No newline at end of file diff --git a/Editor/Obfuscation/Service/ObfuscationGenerator.cs b/Editor/Obfuscation/Service/ObfuscationGenerator.cs index 4dca35d..2027015 100644 --- a/Editor/Obfuscation/Service/ObfuscationGenerator.cs +++ b/Editor/Obfuscation/Service/ObfuscationGenerator.cs @@ -85,12 +85,12 @@ static string WriteObfuscatedAppleClassAsAsset() static string WriteObfuscatedAppleClassAsAsset(string certPath, string classIncompleteErr, string classPrefix) { string appleError = null; - int key = 0; - int[] order = new int[0]; - byte[] tangled = new byte[0]; + var key = 0; + var order = new int[0]; + var tangled = new byte[0]; try { - byte[] bytes = File.ReadAllBytes(certPath); + var bytes = File.ReadAllBytes(certPath); order = new int[bytes.Length / 20 + 1]; // TODO: Integrate with upgraded Tangle! @@ -111,9 +111,9 @@ static string WriteObfuscatedAppleClassAsAsset(string certPath, string classInco static string WriteObfuscatedGooglePlayClassAsAsset(string googlePlayPublicKey) { string googleError = null; - int key = 0; - int[] order = new int[0]; - byte[] tangled = new byte[0]; + var key = 0; + var order = new int[0]; + var tangled = new byte[0]; try { var bytes = Convert.FromBase64String(googlePlayPublicKey); @@ -155,7 +155,7 @@ static bool ObfuscatedClassExists(string classnamePrefix) static void WriteObfuscatedClassAsAsset(string classnamePrefix, int key, int[] order, byte[] data, bool populated) { - Dictionary substitutionDictionary = new Dictionary() + var substitutionDictionary = new Dictionary() { {"{NAME}", classnamePrefix.ToString()}, {"{KEY}", key.ToString()}, @@ -164,19 +164,18 @@ static void WriteObfuscatedClassAsAsset(string classnamePrefix, int key, int[] o {"{POPULATED}", populated.ToString().ToLowerInvariant()} // Defaults to XML-friendly values }; - string templateRelativePath = null; - string templateText = LoadTemplateText(out templateRelativePath); + var templateText = LoadTemplateText(out var templateRelativePath); if (templateText != null) { - string outfileText = templateText; + var outfileText = templateText; // Apply the parameters to the template foreach (var pair in substitutionDictionary) { outfileText = outfileText.Replace(pair.Key, pair.Value); } - Directory.CreateDirectory (TangleFileConsts.k_OutputPath); + Directory.CreateDirectory(TangleFileConsts.k_OutputPath); File.WriteAllText(FullPathForTangleClass(classnamePrefix), outfileText); } } @@ -188,7 +187,7 @@ static void WriteObfuscatedClassAsAsset(string classnamePrefix, int key, int[] o /// Relative Assets/ path to template file. static string LoadTemplateText(out string templateRelativePath) { - string[] assetGUIDs = + var assetGUIDs = AssetDatabase.FindAssets(m_GeneratedCredentialsTemplateFilenameNoExtension); string templateGUID = null; templateRelativePath = null; @@ -209,7 +208,7 @@ static string LoadTemplateText(out string templateRelativePath) { templateRelativePath = AssetDatabase.GUIDToAssetPath(templateGUID); - string templateAbsolutePath = + var templateAbsolutePath = Path.GetDirectoryName(Application.dataPath) + Path.DirectorySeparatorChar + templateRelativePath; diff --git a/Editor/Obfuscation/Service/ObfuscationMigration.cs b/Editor/Obfuscation/Service/ObfuscationMigration.cs index db9b74e..5a13c54 100644 --- a/Editor/Obfuscation/Service/ObfuscationMigration.cs +++ b/Editor/Obfuscation/Service/ObfuscationMigration.cs @@ -32,7 +32,7 @@ internal static void MigrateObfuscations() private static void MoveObfuscatorFiles(string oldPath) { - Directory.CreateDirectory (TangleFileConsts.k_OutputPath); + Directory.CreateDirectory(TangleFileConsts.k_OutputPath); foreach (var prevFile in Directory.GetFiles(oldPath)) { @@ -40,7 +40,7 @@ private static void MoveObfuscatorFiles(string oldPath) } } - static void MoveObfuscatorFile(string file) + static void MoveObfuscatorFile(string file) { var fileName = Path.GetFileName(file); if (fileName.EndsWith(TangleFileConsts.k_ObfuscationClassSuffix)) @@ -56,12 +56,12 @@ static void MoveObfuscatorFile(string file) internal static bool CheckPreviousObfuscationFilesExist() { - return (Directory.Exists(TangleFileConsts.k_PrevOutputPath) && (Directory.GetFiles(TangleFileConsts.k_PrevOutputPath).Length > 0)); + return Directory.Exists(TangleFileConsts.k_PrevOutputPath) && (Directory.GetFiles(TangleFileConsts.k_PrevOutputPath).Length > 0); } internal static bool CheckBadObfuscationFilesExist() { - return (Directory.Exists(TangleFileConsts.k_BadOutputPath) && (Directory.GetFiles(TangleFileConsts.k_BadOutputPath).Length > 0)); + return Directory.Exists(TangleFileConsts.k_BadOutputPath) && (Directory.GetFiles(TangleFileConsts.k_BadOutputPath).Length > 0); } } } diff --git a/Editor/Obfuscation/Service/TangleObfuscator.cs b/Editor/Obfuscation/Service/TangleObfuscator.cs index 2918247..1fc7a94 100644 --- a/Editor/Obfuscation/Service/TangleObfuscator.cs +++ b/Editor/Obfuscation/Service/TangleObfuscator.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; namespace UnityEditor.Purchasing { @@ -12,7 +12,7 @@ public static class TangleObfuscator /// /// An Exception thrown when the tangle order array provided is invalid or shorter than the number of data slices made. /// - public class InvalidOrderArray : Exception {} + public class InvalidOrderArray : Exception { } /// /// Generates the obfucscation tangle data. @@ -23,30 +23,30 @@ public class InvalidOrderArray : Exception {} /// The obfucated public key public static byte[] Obfuscate(byte[] data, int[] order, out int rkey) { - var rnd = new System.Random(); - int key = rnd.Next(2, 255); - byte[] res = new byte[data.Length]; - int slices = data.Length / 20 + 1; + var rnd = new Random(); + var key = rnd.Next(2, 255); + var res = new byte[data.Length]; + var slices = data.Length / 20 + 1; if (order == null || order.Length < slices) { - throw new InvalidOrderArray(); - } + throw new InvalidOrderArray(); + } Array.Copy(data, res, data.Length); - for (int i = 0; i < slices - 1; i ++) + for (var i = 0; i < slices - 1; i++) { - int j = rnd.Next(i, slices - 1); + var j = rnd.Next(i, slices - 1); order[i] = j; - int sliceSize = 20; // prob should be configurable + var sliceSize = 20; // prob should be configurable var tmp = res.Skip(i * 20).Take(sliceSize).ToArray(); // tmp = res[i*20 .. slice] - Array.Copy(res, j * 20, res, i * 20, sliceSize); // res[i] = res[j*20 .. slice] - Array.Copy(tmp, 0, res, j * 20, sliceSize); // res[j] = tmp + Array.Copy(res, j * 20, res, i * 20, sliceSize); // res[i] = res[j*20 .. slice] + Array.Copy(tmp, 0, res, j * 20, sliceSize); // res[j] = tmp } order[slices - 1] = slices - 1; rkey = key; - return res.Select(x => (byte)(x ^ key)).ToArray(); + return res.Select(x => (byte)(x ^ key)).ToArray(); } } } diff --git a/Editor/Obfuscation/UI/ObfuscatorWindow.cs b/Editor/Obfuscation/UI/ObfuscatorWindow.cs index 12e8bf9..a43cd1b 100644 --- a/Editor/Obfuscation/UI/ObfuscatorWindow.cs +++ b/Editor/Obfuscation/UI/ObfuscatorWindow.cs @@ -1,3 +1,4 @@ +using Unity.Services.Core.Editor.OrganizationHandler; using UnityEngine; using static UnityEditor.Purchasing.UnityPurchasingEditor; @@ -43,7 +44,6 @@ internal class ObfuscatorWindow : RichEditorWindow "4. To ensure correct revenue data, enter your key in the Analytics dashboard."; private const string kLabelDashboardLink = "\tOpen Analytics Dashboard"; - private const string kDashboardURL = "https://analytics.cloud.unity3d.com/projects//edit/"; private GUIStyle m_ErrorStyle; private string m_GoogleError; @@ -54,12 +54,12 @@ internal class ObfuscatorWindow : RichEditorWindow /// string m_GooglePlayPublicKey = kPublicKeyPlaceholder; - #if !ENABLE_EDITOR_GAME_SERVICES +#if !ENABLE_EDITOR_GAME_SERVICES [MenuItem(IapMenuConsts.MenuItemRoot + "/Receipt Validation Obfuscator...", false, 200)] static void Init() { // Get existing open window or if none, make a new one: - ObfuscatorWindow window = (ObfuscatorWindow) EditorWindow.GetWindow(typeof(ObfuscatorWindow)); + var window = (ObfuscatorWindow)GetWindow(typeof(ObfuscatorWindow)); window.titleContent.text = kLabelTitle; window.minSize = new Vector2(340, 180); window.Show(); @@ -67,7 +67,7 @@ static void Init() GenericEditorMenuItemClickEventSenderHelpers.SendIapMenuOpenObfuscatorEvent(); GameServicesEventSenderHelpers.SendTopMenuReceiptValidationObfuscatorEvent(); } - #endif +#endif private ObfuscatorWindow() { @@ -83,7 +83,9 @@ void OnGUI() // Apple error message, if any if (!string.IsNullOrEmpty(m_AppleError)) + { GUILayout.Label(m_AppleError, m_ErrorStyle); + } // Google Play GUILayout.Label(kLabelGoogleKey, EditorStyles.boldLabel); @@ -102,17 +104,23 @@ void OnGUI() GUILayout.Label(kObfuscateKeyInstructions); if (!string.IsNullOrEmpty(m_GoogleError)) + { GUILayout.Label(m_GoogleError, m_ErrorStyle); + } + if (GUILayout.Button(kLabelGenerateGoogle)) + { ObfuscateSecrets(includeGoogle: true); + } GUILayout.Label(kDashboardInstructions); -#if UNITY_2018_1_OR_NEWER - GUILink(kLabelDashboardLink, kDashboardURL.Replace("", CloudProjectSettings.projectId)); -#else - GUILink(kLabelDashboardLink, kDashboardURL.Replace("", PlayerSettings.cloudProjectId)); -#endif + GUILink(kLabelDashboardLink, GetFormattedDashboardUrl()); + } + + static string GetFormattedDashboardUrl() + { + return $"https://dashboard.unity3d.com/organizations/{OrganizationProvider.Organization.Key}/projects/{CloudProjectSettings.projectId}/analytics/v2/dashboards/revenue"; } void ObfuscateSecrets(bool includeGoogle) diff --git a/Editor/ProductCatalogEditor.cs b/Editor/ProductCatalogEditor.cs index c875d54..c31a1c8 100644 --- a/Editor/ProductCatalogEditor.cs +++ b/Editor/ProductCatalogEditor.cs @@ -1,6 +1,6 @@ using System; -using System.IO; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using UnityEditor.Connect; @@ -16,7 +16,8 @@ public class ProductCatalogEditor : EditorWindow { private const bool kValidateDebugLog = false; - private static string[] kStoreKeys = { + private static readonly string[] kStoreKeys = + { AppleAppStore.Name, GooglePlay.Name, AmazonApps.Name, @@ -36,24 +37,21 @@ public class ProductCatalogEditor : EditorWindow [MenuItem(ProductCatalogEditorMenuPath, false, 200)] public static void ShowWindow() { - EditorWindow.GetWindow(typeof(ProductCatalogEditor)); + GetWindow(typeof(ProductCatalogEditor)); GenericEditorMenuItemClickEventSenderHelpers.SendIapMenuOpenCatalogEvent(); GameServicesEventSenderHelpers.SendTopMenuIapCatalogEvent(); } - private static GUIContent windowTitle = new GUIContent("IAP Catalog"); - private static List productEditors = new List(); - private static List toRemove = new List(); - private ProductCatalog catalog; + private static readonly GUIContent windowTitle = new GUIContent("IAP Catalog"); + private static readonly List productEditors = new List(); + private static readonly List toRemove = new List(); private Rect exportButtonRect; private ExporterValidationResults validation; - private bool enableCodelessAutoInitialization; private DateTime lastChanged; private bool dirty; - private readonly TimeSpan kSaveDelay = new TimeSpan (0, 0, 0, 0, 500); // 500 milliseconds - + private readonly TimeSpan kSaveDelay = new TimeSpan(0, 0, 0, 0, 500); // 500 milliseconds #region UDP Related Fields @@ -63,10 +61,10 @@ public static void ShowWindow() private static readonly Queue requestQueue = new Queue(); private static bool kIsPreparing = true; - private static TokenInfo kTokenInfo = new TokenInfo(); + private static TokenInfo kTokenInfo = new TokenInfo(); private static string kOrgId; private static object kAppStoreSettings; //UDP AppStoreSettings via Reflection - private static IDictionary kIapItems = new Dictionary(); + private static readonly IDictionary kIapItems = new Dictionary(); private static readonly bool s_udpAvailable = UdpSynchronizationApi.CheckUdpAvailability(); private static string kUdpErrorMsg = ""; @@ -81,7 +79,8 @@ internal static void MigrateProductCatalog() { try { - FileInfo file = new FileInfo(ProductCatalog.kCatalogPath); + var file = new FileInfo(ProductCatalog.kCatalogPath); + // This will create the new product catalog file location, if it already exists, // this will not do anything. file.Directory.Create(); @@ -97,7 +96,6 @@ internal static void MigrateProductCatalog() { AssetDatabase.MoveAsset(ProductCatalog.kPrevCatalogPath, ProductCatalog.kCatalogPath); } - } catch (Exception ex) { @@ -113,13 +111,7 @@ internal static bool DoesPrevCatalogPathExist() /// /// Property which gets the ProductCatalog instance which is being edited. /// - public ProductCatalog Catalog - { - get - { - return catalog; - } - } + public ProductCatalog Catalog { get; private set; } /// /// Sets the results of the validation of catalog items upon export. @@ -133,7 +125,7 @@ public void SetCatalogValidationResults(ExporterValidationResults catalogResults if (productEditors.Count == itemResults.Count) { - for (int i = 0; i < productEditors.Count; ++i) + for (var i = 0; i < productEditors.Count; ++i) { productEditors[i].SetValidationResults(itemResults[i]); } @@ -142,8 +134,8 @@ public void SetCatalogValidationResults(ExporterValidationResults catalogResults void Awake() { - catalog = ProductCatalog.LoadDefaultCatalog(); - if (catalog.allProducts.Count == 0) + Catalog = ProductCatalog.LoadDefaultCatalog(); + if (Catalog.allProducts.Count == 0) { AddNewProduct(); // Start the catalog with one item } @@ -154,29 +146,27 @@ void OnEnable() titleContent = windowTitle; productEditors.Clear(); - foreach (var product in catalog.allProducts) + foreach (var product in Catalog.allProducts) { productEditors.Add(new ProductCatalogItemEditor(product)); } - enableCodelessAutoInitialization = catalog.enableCodelessAutoInitialization; - if (s_udpAvailable && IsUdpInstalled()) { - kUdpErrorMsg = ""; - kTokenInfo = new TokenInfo(); + kUdpErrorMsg = ""; + kTokenInfo = new TokenInfo(); kValidLogin = true; - kValidConfig = true; - kIsPreparing = true; - kOrgId = null; - PrepareDeveloperInfo(); + kValidConfig = true; + kIsPreparing = true; + kOrgId = null; + PrepareDeveloperInfo(); } } - private static bool IsUdpInstalled() - { - return UnityPurchasingEditor.IsUdpUmpPackageInstalled() || UnityPurchasingEditor.IsUdpAssetStorePackageInstalled(); - } + private static bool IsUdpInstalled() + { + return UnityPurchasingEditor.IsUdpUmpPackageInstalled() || UnityPurchasingEditor.IsUdpAssetStorePackageInstalled(); + } private void OnDisable() { @@ -205,7 +195,7 @@ private void SetDirtyFlag() private void Save() { dirty = false; - File.WriteAllText(ProductCatalog.kCatalogPath, ProductCatalog.Serialize(catalog)); + File.WriteAllText(ProductCatalog.kCatalogPath, ProductCatalog.Serialize(Catalog)); AssetDatabase.ImportAsset(ProductCatalog.kCatalogPath); } @@ -240,13 +230,13 @@ void OnGUI() EditorGUILayout.EndScrollView(); EditorGUILayout.BeginVertical(); - float defaultLabelWidth = EditorGUIUtility.labelWidth; + var defaultLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 315; - bool catalogHasProducts = !catalog.IsEmpty(); + var catalogHasProducts = !Catalog.IsEmpty(); if (catalogHasProducts) { - ShowAndProcessCodelessAutoInitToggleGui(); + ShowAndProcessCodelessAutoInitToggleGuis(); } EditorGUILayout.EndVertical(); @@ -256,8 +246,8 @@ void OnGUI() EditorGUILayout.LabelField("Catalog Export"); - catalog.appleSKU = ShowEditTextFieldGuiAndGetValue("appleSKU", "Apple SKU:", catalog.appleSKU); - catalog.appleTeamID = ShowEditTextFieldGuiAndGetValue("appleTeamID", "Apple Team ID:", catalog.appleTeamID); + Catalog.appleSKU = ShowEditTextFieldGuiAndGetValue("appleSKU", "Apple SKU:", Catalog.appleSKU); + Catalog.appleTeamID = ShowEditTextFieldGuiAndGetValue("appleTeamID", "Apple Team ID:", Catalog.appleTeamID); if (EditorGUI.EndChangeCheck()) { @@ -270,7 +260,7 @@ void OnGUI() ProductCatalogExportWindow.kWidth, EditorGUIUtility.singleLineHeight); if (GUI.Button(exportButtonRect, - new GUIContent("App Store Export", "Export products for bulk import into app store tools."))) + new GUIContent("App Store Export", "Export products for bulk import into app store tools."))) { PopupWindow.Show(exportButtonRect, new ProductCatalogExportWindow(this)); } @@ -282,7 +272,7 @@ void OnGUI() productEditors.RemoveAll(x => toRemove.Contains(x)); foreach (var editor in toRemove) { - catalog.Remove(editor.Item); + Catalog.Remove(editor.Item); } toRemove.Clear(); @@ -290,21 +280,59 @@ void OnGUI() } } - private void ShowAndProcessCodelessAutoInitToggleGui() + private void ShowAndProcessCodelessAutoInitToggleGuis() { EditorGUILayout.Space(); - var oldAutoInitializationToggle = catalog.enableCodelessAutoInitialization; - enableCodelessAutoInitialization = EditorGUILayout.Toggle( + + ShowAndProcessIapAutoInitToggleGui(); + if (Catalog.enableCodelessAutoInitialization) + { + ShowAndProcessUgsAutoInitToggleGui(); + } + + EditorGUILayout.Space(); + } + + private void ShowAndProcessIapAutoInitToggleGui() + { + var newValue = EditorGUILayout.Toggle( new GUIContent("Automatically initialize UnityPurchasing (recommended)", "Automatically start Unity IAP if there are any products defined in this catalog. Uncheck this if you plan to initialize Unity IAP manually in your code."), - enableCodelessAutoInitialization); - catalog.enableCodelessAutoInitialization = enableCodelessAutoInitialization; + Catalog.enableCodelessAutoInitialization); + + UpdateIapAutoInitValue(newValue); + } - if (oldAutoInitializationToggle != enableCodelessAutoInitialization) + private void UpdateIapAutoInitValue(bool newValue) + { + if (newValue != Catalog.enableCodelessAutoInitialization) { - GenericEditorClickCheckboxEventSenderHelpers.SendCatalogAutoInitToggleEvent(enableCodelessAutoInitialization); + Catalog.enableCodelessAutoInitialization = newValue; + + GenericEditorClickCheckboxEventSenderHelpers.SendCatalogAutoInitToggleEvent(newValue); + } + } + + private void ShowAndProcessUgsAutoInitToggleGui() + { + var newValue = EditorGUILayout.Toggle(new GUIContent( + "Automatically initialize Unity Gaming Services", + "This initializes Unity Gaming Services with the default `production` environment.\n" + + "This way of initializing Unity Gaming Services might not be compatible with all other services as they might require special initialization options.\n" + + "If the use of initialization options is needed, Unity Gaming Services should be initialized with the coded API."), + Catalog.enableUnityGamingServicesAutoInitialization); + + UpdateUgsAutoInitValue(newValue); + } + + private void UpdateUgsAutoInitValue(bool newValue) + { + if (newValue != Catalog.enableUnityGamingServicesAutoInitialization) + { + Catalog.enableUnityGamingServicesAutoInitialization = newValue; + + GenericEditorClickCheckboxEventSenderHelpers.SendCatalogUgsAutoInitToggleEvent(newValue); } - EditorGUILayout.Space(); } string ShowEditTextFieldGuiAndGetValue(string fieldName, string label, string oldText) @@ -353,19 +381,22 @@ private void AddNewProduct() } } - if (invalidIdsExist) return; + if (invalidIdsExist) + { + return; + } var newEditor = new ProductCatalogItemEditor(); newEditor.SetShouldBeMarked(false); productEditors.Add(newEditor); - catalog.Add(newEditor.Item); + Catalog.Add(newEditor.Item); } private void CheckForDuplicateIDs() { var ids = new HashSet(); var duplicates = new HashSet(); - foreach (var product in catalog.allProducts) + foreach (var product in Catalog.allProducts) { if (!string.IsNullOrEmpty(product.id) && ids.Contains(product.id)) { @@ -413,11 +444,13 @@ private static void BeginErrorBlock(ExporterValidationResults validation, string private static void EndErrorBlock(ExporterValidationResults validation, string fieldName) { - if (EditorGUI.EndChangeCheck() && validation != null) { + if (EditorGUI.EndChangeCheck() && validation != null) + { validation.fieldErrors.Remove(fieldName); } - if (validation != null && validation.fieldErrors.ContainsKey(fieldName)) { + if (validation != null && validation.fieldErrors.ContainsKey(fieldName)) + { var style = new GUIStyle(); style.normal.textColor = Color.red; EditorGUILayout.LabelField(validation.fieldErrors[fieldName], style); @@ -433,7 +466,7 @@ private static void EndErrorBlock(ExporterValidationResults validation, string f /// Whether or not the export was succesful. Always returns false if eraseExport is true. public static bool Export(string storeName, string folder, bool eraseExport) { - var editor = ScriptableObject.CreateInstance(typeof(ProductCatalogEditor)) as ProductCatalogEditor; + var editor = CreateInstance(typeof(ProductCatalogEditor)) as ProductCatalogEditor; return new ProductCatalogExportWindow(editor).Export(storeName, folder, eraseExport); } @@ -468,45 +501,47 @@ private void CheckApiUpdate() kUdpErrorMsg = ""; } } + // No error. else { if (resp.GetType() == typeof(TokenInfo)) { resp = JsonUtility.FromJson(downloadedRawJson); - kTokenInfo.access_token = ((TokenInfo) resp).access_token; - kTokenInfo.refresh_token = ((TokenInfo) resp).refresh_token; + kTokenInfo.access_token = ((TokenInfo)resp).access_token; + kTokenInfo.refresh_token = ((TokenInfo)resp).refresh_token; var newRequest = UdpSynchronizationApi.CreateGetOrgIdRequest(kTokenInfo.access_token, Application.cloudProjectId); - ReqStruct newReqStruct = new ReqStruct {request = newRequest, resp = new OrgIdResponse()}; + var newReqStruct = new ReqStruct { request = newRequest, resp = new OrgIdResponse() }; requestQueue.Enqueue(newReqStruct); } + // Get orgId request else if (resp.GetType() == typeof(OrgIdResponse)) { resp = JsonUtility.FromJson(downloadedRawJson); - kOrgId = ((OrgIdResponse) resp).org_foreign_key; - - if (kAppStoreSettings != null) - { - var appSlug = AppStoreSettingsInterface.GetAppSlugField(); - - // Then, get all iap items - requestQueue.Enqueue(new ReqStruct - { - request = UdpSynchronizationApi.CreateSearchStoreItemRequest(kTokenInfo.access_token, kOrgId, (string)appSlug.GetValue(kAppStoreSettings)), - resp = new IapItemSearchResponse() - }); - } + kOrgId = ((OrgIdResponse)resp).org_foreign_key; + + if (kAppStoreSettings != null) + { + var appSlug = AppStoreSettingsInterface.GetAppSlugField(); + + // Then, get all iap items + requestQueue.Enqueue(new ReqStruct + { + request = UdpSynchronizationApi.CreateSearchStoreItemRequest(kTokenInfo.access_token, kOrgId, (string)appSlug.GetValue(kAppStoreSettings)), + resp = new IapItemSearchResponse() + }); + } } else if (resp.GetType() == typeof(IapItemSearchResponse)) { if (downloadedRawJson != null) { resp = JsonUtility.FromJson(downloadedRawJson); - foreach (var item in ((IapItemSearchResponse) resp).results) + foreach (var item in ((IapItemSearchResponse)resp).results) { kIapItems[item.slug] = item; } @@ -514,6 +549,7 @@ private void CheckApiUpdate() kIsPreparing = false; } + // Creating/Updating IAP item succeeds else if (resp.GetType() == typeof(IapItemResponse)) { @@ -523,10 +559,11 @@ private void CheckApiUpdate() { reqStruct.itemEditor.udpItemSyncing = false; kIapItems[reqStruct.iapItem.slug] = reqStruct.iapItem; - kIapItems[reqStruct.iapItem.slug].id = ((IapItemResponse) resp).id; + kIapItems[reqStruct.iapItem.slug].id = ((IapItemResponse)resp).id; } } } + Repaint(); } else @@ -540,18 +577,9 @@ void TryParseErrorAsJson(string downloadedRawJson, long responseCode) try { var response = JsonUtility.FromJson(downloadedRawJson); - if (response?.message != null && response.details != null && response.details.Length != 0) - { - kUdpErrorMsg = string.Format("{0} : {1}", response.details[0].field, response.message); - } - else if (response?.message != null) - { - kUdpErrorMsg = response.message; - } - else - { - kUdpErrorMsg = $"Unknown Error, Please try again. Error code: {responseCode}"; - } + kUdpErrorMsg = response?.message != null && response.details != null && response.details.Length != 0 + ? string.Format("{0} : {1}", response.details[0].field, response.message) + : response?.message != null ? response.message : $"Unknown Error, Please try again. Error code: {responseCode}"; } catch (ArgumentException) { @@ -559,18 +587,17 @@ void TryParseErrorAsJson(string downloadedRawJson, long responseCode) } } - /// /// Get userId, orgId of the developer. Make prepare for syncing /// void PrepareDeveloperInfo() { // Get Client ID - Type udpAppStoreSettings = AppStoreSettingsInterface.GetClassType(); + var udpAppStoreSettings = AppStoreSettingsInterface.GetClassType(); if (udpAppStoreSettings != null) { var assetPathProp = AppStoreSettingsInterface.GetAssetPathField(); - var clientIDProp = AppStoreSettingsInterface.GetClientIDField(); + var clientIDProp = AppStoreSettingsInterface.GetClientIDField(); kAppStoreSettings = AssetDatabase.LoadAssetAtPath((string)assetPathProp.GetValue(null), udpAppStoreSettings); @@ -606,14 +633,14 @@ public void GetAuthCode(T response) { var authCodePropertyInfo = response.GetType().GetProperty("AuthCode"); var exceptionPropertyInfo = response.GetType().GetProperty("Exception"); - string authCode = (string) authCodePropertyInfo.GetValue(response, null); - Exception exception = (Exception) exceptionPropertyInfo.GetValue(response, null); + var authCode = (string)authCodePropertyInfo.GetValue(response, null); + var exception = (Exception)exceptionPropertyInfo.GetValue(response, null); if (authCode != null) { var request = UdpSynchronizationApi.CreateGetAccessTokenRequest(authCode); - TokenInfo tokenInfoResp = new TokenInfo(); - ReqStruct reqStruct = new ReqStruct {request = request, resp = tokenInfoResp}; + var tokenInfoResp = new TokenInfo(); + var reqStruct = new ReqStruct { request = request, resp = tokenInfoResp }; requestQueue.Enqueue(reqStruct); } else @@ -639,7 +666,7 @@ public class ProductCatalogItemEditor private ExporterValidationResults validation; - private bool editorSupportsPayouts = false; + private readonly bool editorSupportsPayouts = false; private bool advancedVisible = true; private bool descriptionVisible = true; @@ -663,17 +690,17 @@ public class ProductCatalogItemEditor /// public string udpSyncErrorMsg = ""; - private List descriptionsToRemove = new List(); - private List payoutsToRemove = new List(); + private readonly List descriptionsToRemove = new List(); + private readonly List payoutsToRemove = new List(); /// /// Default constructor. Creates a new ProductCatalogItem to edit. /// public ProductCatalogItemEditor() { - this.Item = new ProductCatalogItem(); + Item = new ProductCatalogItem(); - editorSupportsPayouts = (null != typeof(ProductDefinition).GetProperty("payouts")); + editorSupportsPayouts = null != typeof(ProductDefinition).GetProperty("payouts"); } /// @@ -682,8 +709,8 @@ public ProductCatalogItemEditor() /// The description of the item being created. public ProductCatalogItemEditor(ProductCatalogItem description) { - this.Item = description; - editorSupportsPayouts = (null != typeof(ProductDefinition).GetProperty("payouts")); + Item = description; + editorSupportsPayouts = null != typeof(ProductDefinition).GetProperty("payouts"); } /// @@ -691,11 +718,11 @@ public ProductCatalogItemEditor(ProductCatalogItem description) /// public void OnGUI() { - GUIStyle s = new GUIStyle(EditorStyles.foldout); + var s = new GUIStyle(EditorStyles.foldout); var box = EditorGUILayout.BeginVertical(); - Rect rect = new Rect(box.xMax - EditorGUIUtility.singleLineHeight - 2, box.yMin, EditorGUIUtility.singleLineHeight + 2, EditorGUIUtility.singleLineHeight); - if (GUI.Button(rect, "x") && EditorUtility.DisplayDialog("Delete Product?", "Are you sure you want to delete this product?","Delete","Do Not Delete")) + var rect = new Rect(box.xMax - EditorGUIUtility.singleLineHeight - 2, box.yMin, EditorGUIUtility.singleLineHeight + 2, EditorGUIUtility.singleLineHeight); + if (GUI.Button(rect, "x") && EditorUtility.DisplayDialog("Delete Product?", "Are you sure you want to delete this product?", "Delete", "Do Not Delete")) { toRemove.Add(this); GenericEditorButtonClickEventSenderHelpers.SendCatalogRemoveProductEvent(); @@ -704,13 +731,15 @@ public void OnGUI() ShowValidationResultsGUI(validation); var productLabel = Item.id + (string.IsNullOrEmpty(Item.defaultDescription.Title) - ? string.Empty - : " - " + Item.defaultDescription.Title); + ? string.Empty + : " - " + Item.defaultDescription.Title); if (string.IsNullOrEmpty(productLabel) || Item.id.Trim().Length == 0) { productLabel = "Product ID is Empty"; - } else { + } + else + { idInvalid = false; } @@ -787,7 +816,7 @@ public void OnGUI() { EditorGUI.indentLevel++; - int payoutIndex = 1; + var payoutIndex = 1; foreach (var payout in Item.Payouts) { var payoutBox = EditorGUILayout.BeginVertical(); @@ -831,13 +860,16 @@ public void OnGUI() EditorGUILayout.Separator(); storeIDsVisible = CompatibleGUI.Foldout(storeIDsVisible, "Store ID Overrides", true, s); - if (storeIDsVisible) { + + if (storeIDsVisible) + { EditorGUI.indentLevel++; - foreach (string storeKey in kStoreKeys) + foreach (var storeKey in kStoreKeys) { var newStoreID = ShowEditTextFieldGuiWithValidationErrorBlockAndGetValue("storeID." + storeKey, storeKey, Item.GetStoreID(storeKey)); Item.SetStoreID(storeKey, newStoreID); } + EditorGUI.indentLevel--; } @@ -879,22 +911,24 @@ public void OnGUI() { EditorGUI.indentLevel++; - if (!string.IsNullOrEmpty(udpSyncErrorMsg)){ + if (!string.IsNullOrEmpty(udpSyncErrorMsg)) + { var errStyle = new GUIStyle(); errStyle.normal.textColor = Color.red; EditorGUILayout.LabelField(udpSyncErrorMsg, errStyle); } - var udpFieldsDisabled = kIsPreparing || udpItemSyncing || !kValidLogin || !kValidConfig; + var udpFieldsDisabled = kIsPreparing || udpItemSyncing || !kValidLogin || !kValidConfig; - //If everything appears ok, check UDP compatibility and warn user if there's a problem - //This should not stop the user from doing some UDP sync work, as there is no current blocker for those features. - if (!udpFieldsDisabled && string.IsNullOrEmpty(kUdpErrorMsg) && !UdpSynchronizationApi.CheckUdpCompatibility()) - { - kUdpErrorMsg = "Please update your UDP package. Transaction features will no longer work at runtime with your current UDP version"; - } + //If everything appears ok, check UDP compatibility and warn user if there's a problem + //This should not stop the user from doing some UDP sync work, as there is no current blocker for those features. + if (!udpFieldsDisabled && string.IsNullOrEmpty(kUdpErrorMsg) && !UdpSynchronizationApi.CheckUdpCompatibility()) + { + kUdpErrorMsg = "Please update your UDP package. Transaction features will no longer work at runtime with your current UDP version"; + } - if (!string.IsNullOrEmpty(kUdpErrorMsg)){ + if (!string.IsNullOrEmpty(kUdpErrorMsg)) + { var errStyle = new GUIStyle(); errStyle.normal.textColor = Color.red; EditorGUILayout.LabelField(kUdpErrorMsg, errStyle); @@ -911,36 +945,30 @@ public void OnGUI() ? string.Empty : Item.udpPrice.value.ToString()); - decimal priceDecimal; - if (decimal.TryParse(priceStr, out priceDecimal)) - { - Item.udpPrice.value = priceDecimal; - } - else - { - Item.udpPrice.value = 0; - } + Item.udpPrice.value = decimal.TryParse(priceStr, out var priceDecimal) ? priceDecimal : 0; EndErrorBlock(validation, "udpPrice"); if (GUILayout.Button("Sync to UDP")) { udpSyncErrorMsg = ""; - IapItem iapItem = new IapItem(); - iapItem.consumable = Item.type == ProductType.Consumable; - iapItem.slug = Item.GetStoreID(UDP.Name) ?? Item.id; - iapItem.name = Item.defaultDescription.Title; + var iapItem = new IapItem + { + consumable = Item.type == ProductType.Consumable, + slug = Item.GetStoreID(UDP.Name) ?? Item.id, + name = Item.defaultDescription.Title + }; iapItem.properties.description = Item.defaultDescription.Description; iapItem.priceSets.PurchaseFee.priceMap.DEFAULT.Add(new PriceDetail { price = Item.udpPrice.value.ToString() }); - if (kAppStoreSettings != null) - { - var appSlug = AppStoreSettingsInterface.GetAppSlugField(); - iapItem.masterItemSlug = (string)appSlug.GetValue(kAppStoreSettings); - } + if (kAppStoreSettings != null) + { + var appSlug = AppStoreSettingsInterface.GetAppSlugField(); + iapItem.masterItemSlug = (string)appSlug.GetValue(kAppStoreSettings); + } iapItem.ownerId = kOrgId; @@ -985,6 +1013,7 @@ public void OnGUI() EditorGUI.indentLevel--; } } + #endregion EditorGUI.indentLevel--; @@ -1008,7 +1037,7 @@ void ShowAndProcessProductIDBlockGui(Rect idRect) var style = new GUIStyle(); style.normal.textColor = Color.red; - var duplicateIDLabelRect = new Rect(idRect.xMax + 5, idRect.yMin, k_DuplicateIDFieldWidth,EditorGUIUtility.singleLineHeight); + var duplicateIDLabelRect = new Rect(idRect.xMax + 5, idRect.yMin, k_DuplicateIDFieldWidth, EditorGUIUtility.singleLineHeight); EditorGUI.LabelField(duplicateIDLabelRect, idDuplicate ? "ID is a duplicate" : string.Empty, style); EditorGUI.LabelField(duplicateIDLabelRect, idDuplicate && idInvalid && shouldBeMarked ? "ID is empty" : string.Empty, style); @@ -1024,7 +1053,7 @@ void ShowAndProcessProductTypeBlockGui(float width) var typeRect = EditorGUILayout.GetControlRect(true); typeRect.width = width; - Item.type = (ProductType) EditorGUI.EnumPopup(typeRect, "Type:", Item.type); + Item.type = (ProductType)EditorGUI.EnumPopup(typeRect, "Type:", Item.type); if (oldType != Item.type) { @@ -1038,7 +1067,7 @@ void ShowAndProcessProductTypeBlockGui(float width) void ShowAndProcessPayoutBlockGui(ProductCatalogPayout payout) { var oldType = payout.type; - payout.type = (ProductCatalogPayout.ProductCatalogPayoutType) EditorGUILayout.EnumPopup("Type", payout.type); + payout.type = (ProductCatalogPayout.ProductCatalogPayoutType)EditorGUILayout.EnumPopup("Type", payout.type); if (oldType != payout.type) { var typeName = Enum.GetName(typeof(ProductCatalogPayout.ProductCatalogPayoutType), payout.type); @@ -1047,7 +1076,7 @@ void ShowAndProcessPayoutBlockGui(ProductCatalogPayout payout) payout.subtype = TruncateString(ShowEditTextFieldGuiAndGetValue("payoutSubtype", "Subtype", payout.subtype), ProductCatalogPayout.MaxSubtypeLength); payout.quantity = ShowEditDoubleFieldGuiAndGetValue("payoutQuantity", "Quantity", payout.quantity); - payout.data = TruncateString(ShowEditTextFieldGuiAndGetValue("payoutData","Data", payout.data), ProductCatalogPayout.MaxDataLength); + payout.data = TruncateString(ShowEditTextFieldGuiAndGetValue("payoutData", "Data", payout.data), ProductCatalogPayout.MaxDataLength); } void ShowAndProcessGoogleConfigGui() @@ -1058,17 +1087,9 @@ void ShowAndProcessGoogleConfigGui() BeginErrorBlock(validation, fieldName); var priceStr = ShowEditTextFieldGuiAndGetValue(fieldName, "Price:", Item.googlePrice == null || Item.googlePrice.value == 0 ? string.Empty : Item.googlePrice.value.ToString()); - decimal priceDecimal; - if (decimal.TryParse(priceStr, out priceDecimal)) - { - Item.googlePrice.value = priceDecimal; - } - else - { - Item.googlePrice.value = 0; - } + Item.googlePrice.value = decimal.TryParse(priceStr, out var priceDecimal) ? priceDecimal : 0; - Item.pricingTemplateID = ShowEditTextFieldGuiAndGetValue("googlePriceTemplate","Pricing Template:", Item.pricingTemplateID); + Item.pricingTemplateID = ShowEditTextFieldGuiAndGetValue("googlePriceTemplate", "Pricing Template:", Item.pricingTemplateID); EndErrorBlock(validation, fieldName); } @@ -1097,7 +1118,7 @@ void ShowAndProcessAppleConfigGui() EditorGUIUtility.singleLineHeight); if (GUI.Button(screenshotButtonRect, new GUIContent("Select a screenshot", "Required for Apple XML Delivery."))) { - string selectedPath = EditorUtility.OpenFilePanel("Select a screenshot", "", ""); + var selectedPath = EditorUtility.OpenFilePanel("Select a screenshot", "", ""); if (selectedPath != null) { Item.screenshotPath = selectedPath; @@ -1107,10 +1128,8 @@ void ShowAndProcessAppleConfigGui() } EditorGUILayout.EndVertical(); - } - /// /// Sets the validation results upon export of this item. /// @@ -1162,7 +1181,8 @@ private bool DescriptionEditorGUI(LocalizedProductDescription description, bool var removeButtonWidth = EditorGUIUtility.singleLineHeight + 2; var rect = EditorGUILayout.GetControlRect(true); - if (showRemoveButton) { + if (showRemoveButton) + { rect.width -= removeButtonWidth; } @@ -1172,12 +1192,12 @@ private bool DescriptionEditorGUI(LocalizedProductDescription description, bool description.Description = ShowEditTextFieldGuiWithValidationErrorBlockAndGetValue(fieldValidationPrefix + ".Description", "Description:", description.Description); var removeButtonRect = new Rect(box.xMax - removeButtonWidth, box.yMin, removeButtonWidth, EditorGUIUtility.singleLineHeight); - var remove = (showRemoveButton - && GUI.Button(removeButtonRect, "x") - && EditorUtility.DisplayDialog("Delete Translation?", - "Are you sure you want to delete this translation?", - "Delete", - "Do Not Delete")); + var remove = showRemoveButton + && GUI.Button(removeButtonRect, "x") + && EditorUtility.DisplayDialog("Delete Translation?", + "Are you sure you want to delete this translation?", + "Delete", + "Do Not Delete"); EditorGUILayout.EndVertical(); return remove; } @@ -1187,7 +1207,7 @@ void ShowAndProcessLocaleBlockGui(LocalizedProductDescription description, strin BeginErrorBlock(validation, fieldValidationPrefix + ".googleLocale"); var oldLocale = description.googleLocale; - description.googleLocale = (TranslationLocale)EditorGUI.Popup(rect, "Locale:", (int)description.googleLocale,LocaleExtensions.GetLabelsWithSupportedPlatforms()); + description.googleLocale = (TranslationLocale)EditorGUI.Popup(rect, "Locale:", (int)description.googleLocale, LocaleExtensions.GetLabelsWithSupportedPlatforms()); if (oldLocale != description.googleLocale) { var localeName = Enum.GetName(typeof(TranslationLocale), description.googleLocale); @@ -1230,11 +1250,19 @@ double ShowEditDoubleFieldGuiAndGetValue(string fieldName, string label, double return newAmount; } - private static string TruncateString (string s, int len) + private static string TruncateString(string s, int len) { - if (string.IsNullOrEmpty (s)) return s; - if (len < 0) return string.Empty; - return s.Substring (0, Math.Min (s.Length, len)); + if (string.IsNullOrEmpty(s)) + { + return s; + } + + if (len < 0) + { + return string.Empty; + } + + return s.Substring(0, Math.Min(s.Length, len)); } } @@ -1248,8 +1276,8 @@ public class ProductCatalogExportWindow : PopupWindowContent /// public const float kWidth = 200f; - private ProductCatalogEditor editor; - private List exporters = new List(); + private readonly ProductCatalogEditor editor; + private readonly List exporters = new List(); /// /// Constructor taking an instance of ProductCatalogEditor to export contents from. @@ -1301,7 +1329,7 @@ public override void OnGUI(Rect rect) private bool Validate(IProductCatalogExporter exporter, out ExporterValidationResults catalogValidation, out List itemValidation, bool debug = false) { - bool valid = true; + var valid = true; catalogValidation = exporter.Validate(editor.Catalog); valid = valid && catalogValidation.Valid; itemValidation = new List(); @@ -1315,27 +1343,34 @@ private bool Validate(IProductCatalogExporter exporter, out ExporterValidationRe if (debug) { - Action DebugResults = - (string name, ExporterValidationResults r) => + void DebugResults(string name, ExporterValidationResults r) + { + if (!r.Valid || r.warnings.Count != 0) { - if (!r.Valid || r.warnings.Count != 0) Debug.LogWarning(name + ", Valid = " + r.Valid); - foreach (var m in r.errors) - { - Debug.LogWarning("errors " + m); - } + Debug.LogWarning(name + ", Valid = " + r.Valid); + } - foreach (var m in r.fieldErrors) - { - Debug.LogWarning("fieldErrors " + m); - } + foreach (var m in r.errors) + { + Debug.LogWarning("errors " + m); + } - foreach (var m in r.warnings) - { - Debug.LogWarning("warnings " + m); - } - }; + foreach (var m in r.fieldErrors) + { + Debug.LogWarning("fieldErrors " + m); + } + + foreach (var m in r.warnings) + { + Debug.LogWarning("warnings " + m); + } + } + + if (!valid) + { + Debug.LogWarning("Product Catalog Export Overall Result: valid " + valid); + } - if (!valid) Debug.LogWarning("Product Catalog Export Overall Result: valid " + valid); DebugResults("CatalogValidation", catalogValidation); foreach (var r in itemValidation) { @@ -1348,10 +1383,8 @@ private bool Validate(IProductCatalogExporter exporter, out ExporterValidationRe private void Export(IProductCatalogExporter exporter) { - ExporterValidationResults catalogValidation; - List itemValidation; - var valid = Validate(exporter, out catalogValidation, out itemValidation, kValidateDebugLog); + var valid = Validate(exporter, out var catalogValidation, out var itemValidation, kValidateDebugLog); editor.SetCatalogValidationResults(catalogValidation, itemValidation); if (valid) @@ -1364,6 +1397,7 @@ private void Export(IProductCatalogExporter exporter) // Choose the location of the final directory var directoryPath = EditorUtility.SaveFolderPanel("Export to folder", "", ""); directoryPath = Path.Combine(directoryPath, exporter.MandatoryExportFolder); + // Replace any existing directory if (Directory.Exists(directoryPath)) { @@ -1371,6 +1405,7 @@ private void Export(IProductCatalogExporter exporter) } Directory.CreateDirectory(directoryPath); + // ExportHelper needs a single file, let it create the main file and save the auxilliary files. var mainFilePath = Path.Combine(directoryPath, string.Format("{0}.{1}", exporter.DefaultFileName, exporter.FileExtension)); @@ -1407,7 +1442,7 @@ private void Export(IProductCatalogExporter exporter) EditorUtility.DisplayDialog( "Exported Successfully", string.Format("Exported {0} to \"{2}\".\n\n" + - "Also saved copy into project at \"{1}\".", + "Also saved copy into project at \"{1}\".", exporter.DisplayName, nonInteractivePath, path), "OK"); } @@ -1428,8 +1463,8 @@ private void ExportHelper(IProductCatalogExporter exporter, string path) { foreach (var fileToCopy in exporter.FilesToCopy) { - string targetPath = Path.Combine(Path.GetDirectoryName(path), Path.GetFileName(fileToCopy)); - FileInfo fileInfo = new FileInfo(fileToCopy); + var targetPath = Path.Combine(Path.GetDirectoryName(path), Path.GetFileName(fileToCopy)); + var fileInfo = new FileInfo(fileToCopy); fileInfo.CopyTo(targetPath, true); } } @@ -1447,7 +1482,7 @@ internal bool Export(string storeName, string folder, bool justEraseExport) { var catalog = editor.Catalog; // This may be normalized before export - IProductCatalogExporter exporter = exporters.Single(e => e.StoreName == storeName); + var exporter = exporters.Single(e => e.StoreName == storeName); if (exporter == null) { Debug.LogErrorFormat("Unable to export {0} Product Catalog. Export is unsupported for this store.", @@ -1469,9 +1504,7 @@ internal bool Export(string storeName, string folder, bool justEraseExport) return false; } - ExporterValidationResults catalogValidation; - List itemValidation; - var valid = Validate(exporter, out catalogValidation, out itemValidation, kValidateDebugLog); + var valid = Validate(exporter, out var catalogValidation, out var itemValidation, kValidateDebugLog); if (!valid) { @@ -1481,7 +1514,7 @@ internal bool Export(string storeName, string folder, bool justEraseExport) catalog = exporter.NormalizeToType(catalog); } - bool wrote = false; + var wrote = false; if (!string.IsNullOrEmpty(path)) { @@ -1511,10 +1544,7 @@ public class ExporterValidationResults /// /// Property that checks if the export results are valid. /// - public bool Valid - { - get { return (errors.Count == 0 && fieldErrors.Count == 0); } - } + public bool Valid => errors.Count == 0 && fieldErrors.Count == 0; /// /// The list of errors. @@ -1526,7 +1556,6 @@ public bool Valid /// public List warnings = new List(); - /// /// The dictionary of field errors. /// diff --git a/Editor/RichEditorWindow.cs b/Editor/RichEditorWindow.cs index ec10979..2398f72 100644 --- a/Editor/RichEditorWindow.cs +++ b/Editor/RichEditorWindow.cs @@ -1,4 +1,4 @@ -using UnityEngine; +using UnityEngine; namespace UnityEditor.Purchasing { @@ -17,10 +17,10 @@ internal RichEditorWindow() internal void GUILink(string linkText, string url) { - m_LightLinkIcon = m_LightLinkIcon ?? AssetDatabase.LoadAssetAtPath(kLightLinkIconPath); - m_DarkLinkIcon = m_DarkLinkIcon ?? AssetDatabase.LoadAssetAtPath(kDarkLinkIconPath); + m_LightLinkIcon ??= AssetDatabase.LoadAssetAtPath(kLightLinkIconPath); + m_DarkLinkIcon ??= AssetDatabase.LoadAssetAtPath(kDarkLinkIconPath); - m_LinkStyle = m_LinkStyle ?? new GUIStyle(); + m_LinkStyle ??= new GUIStyle(); m_LinkStyle.normal.textColor = EditorGUIUtility.isProSkin ? Color.cyan : Color.blue; m_LinkStyle.contentOffset = new Vector2(6, 0); // Indent like other labels @@ -31,14 +31,18 @@ internal void GUILink(string linkText, string url) var linkRect = GUILayoutUtility.GetLastRect(); if (linkIcon != null) + { GUI.Label(new Rect(linkSize.x, linkRect.y, linkRect.height, linkRect.height), linkIcon); + } else { Debug.LogWarning("Cannot get icon: " + kLightLinkIconPath); } if (Event.current.type == EventType.MouseUp && linkRect.Contains(Event.current.mousePosition)) + { Application.OpenURL(url); + } } } diff --git a/Editor/ServiceProjectSettings/Entity/Consts/UIResourceUtils.cs b/Editor/ServiceProjectSettings/Entity/Consts/UIResourceUtils.cs index 6da2773..b574161 100644 --- a/Editor/ServiceProjectSettings/Entity/Consts/UIResourceUtils.cs +++ b/Editor/ServiceProjectSettings/Entity/Consts/UIResourceUtils.cs @@ -6,11 +6,11 @@ static class UIResourceUtils internal static readonly string labelUxmlPath = $"{SettingsUIConstants.packageUxmlRoot}/Label.uxml"; + internal static readonly string analyticsWarningUxmlPath = $"{SettingsUIConstants.packageUxmlRoot}/AnalyticsWarning.uxml"; internal static readonly string catalogUxmlPath = $"{SettingsUIConstants.packageUxmlRoot}/CatalogEditor.uxml"; internal static readonly string platformSupportUxmlPath = $"{SettingsUIConstants.packageUxmlRoot}/PlatformSupportVisual.uxml"; internal static readonly string googlePlayConfigUxmlPath = $"{SettingsUIConstants.packageUxmlRoot}/GooglePlayConfiguration.uxml"; internal static readonly string appleConfigUxmlPath = $"{SettingsUIConstants.packageUxmlRoot}/AppleConfiguration.uxml"; - internal static readonly string analyticsNoticeUxmlPath = $"{SettingsUIConstants.packageUxmlRoot}/AnalyticsNotice.uxml"; internal static readonly string platformSupportCommonUssPath = $"{SettingsUIConstants.packageUssRoot}/PlatformSupportVisualCommon.uss"; internal static readonly string platformSupportDarkUssPath = $"{SettingsUIConstants.packageUssRoot}/PlatformSupportVisualDark.uss"; diff --git a/Editor/ServiceProjectSettings/Presenter/BasePurchasingState.cs b/Editor/ServiceProjectSettings/Presenter/BasePurchasingState.cs index d3f4f82..ae81ced 100644 --- a/Editor/ServiceProjectSettings/Presenter/BasePurchasingState.cs +++ b/Editor/ServiceProjectSettings/Presenter/BasePurchasingState.cs @@ -11,9 +11,11 @@ internal abstract class BasePurchasingState : SimpleStateMachine.State protected BasePurchasingState(string stateName, SimpleStateMachine stateMachine) : base(stateName, stateMachine) { - m_UIBlocks = new List(); - m_UIBlocks.Add(CreateAnalyticsNoticeBlock()); - m_UIBlocks.Add(PlatformsAndStoresServiceSettingsBlock.CreateStateSpecificBlock(IsEnabled())); + m_UIBlocks = new List + { + PlatformsAndStoresServiceSettingsBlock.CreateStateSpecificBlock(IsEnabled()), + new AnalyticsWarningSettingsBlock() + }; } internal List GetStateUI() @@ -21,8 +23,6 @@ internal List GetStateUI() return m_UIBlocks.Select(block => block.GetUIBlockElement()).ToList(); } - protected abstract AnalyticsNoticeBlock CreateAnalyticsNoticeBlock(); - internal abstract bool IsEnabled(); } } diff --git a/Editor/ServiceProjectSettings/Presenter/PurchasingDisabledState.cs b/Editor/ServiceProjectSettings/Presenter/PurchasingDisabledState.cs index 4c6df2f..fea3bf3 100644 --- a/Editor/ServiceProjectSettings/Presenter/PurchasingDisabledState.cs +++ b/Editor/ServiceProjectSettings/Presenter/PurchasingDisabledState.cs @@ -15,11 +15,6 @@ SimpleStateMachine.State HandleEnabling(bool raisedEvent) return stateMachine.GetStateByName(PurchasingEnabledState.k_StateNameEnabled); } - protected override AnalyticsNoticeBlock CreateAnalyticsNoticeBlock() - { - return AnalyticsNoticeBlock.CreateDisabledAnalyticsBlock(); - } - internal override bool IsEnabled() => false; } } diff --git a/Editor/ServiceProjectSettings/Presenter/PurchasingEnabledState.cs b/Editor/ServiceProjectSettings/Presenter/PurchasingEnabledState.cs index ce89f30..390def1 100644 --- a/Editor/ServiceProjectSettings/Presenter/PurchasingEnabledState.cs +++ b/Editor/ServiceProjectSettings/Presenter/PurchasingEnabledState.cs @@ -19,11 +19,6 @@ SimpleStateMachine.State HandleDisabling(bool raisedEvent) return stateMachine.GetStateByName(PurchasingDisabledState.k_StateNameDisabled); } - protected override AnalyticsNoticeBlock CreateAnalyticsNoticeBlock() - { - return AnalyticsNoticeBlock.CreateEnabledAnalyticsBlock(); - } - internal override bool IsEnabled() => true; } } diff --git a/Editor/ServiceProjectSettings/PurchasingGameService.cs b/Editor/ServiceProjectSettings/PurchasingGameService.cs index 3d58bb2..467766b 100644 --- a/Editor/ServiceProjectSettings/PurchasingGameService.cs +++ b/Editor/ServiceProjectSettings/PurchasingGameService.cs @@ -1,6 +1,7 @@ #if SERVICES_SDK_CORE_ENABLED using System; using Unity.Services.Core.Editor; +using Unity.Services.Core.Editor.OrganizationHandler; using UnityEngine; namespace UnityEditor.Purchasing @@ -46,7 +47,7 @@ public void RemoveDisableAction(Action toRemove) public string GetFormattedDashboardUrl() { - return $"https://analytics.cloud.unity3d.com/projects/{CloudProjectSettings.projectId}/purchasing/"; + return $"https://dashboard.unity3d.com/organizations/{OrganizationProvider.Organization.Key}/projects/{CloudProjectSettings.projectId}/analytics/v2/dashboards/revenue"; } public IEditorGameServiceEnabler Enabler => m_Enabler; diff --git a/Editor/ServiceProjectSettings/PurchasingServiceEnabler.cs b/Editor/ServiceProjectSettings/PurchasingServiceEnabler.cs index 588909a..1349a8f 100644 --- a/Editor/ServiceProjectSettings/PurchasingServiceEnabler.cs +++ b/Editor/ServiceProjectSettings/PurchasingServiceEnabler.cs @@ -15,10 +15,7 @@ internal class PurchasingServiceEnabler : EditorGameServiceFlagEnabler const string k_ServiceFlagName = "purchasing"; const string k_LegacyEnabledSettingName = "Purchasing"; - protected override string FlagName - { - get { return k_ServiceFlagName; } - } + protected override string FlagName => k_ServiceFlagName; protected override void EnableLocalSettings() { @@ -42,7 +39,7 @@ static void SetLegacyEnabledSetting(bool value) var setCloudServiceEnabledMethod = playerSettingsType.GetMethod("SetCloudServiceEnabled", BindingFlags.Static | BindingFlags.NonPublic); if (setCloudServiceEnabledMethod != null) { - setCloudServiceEnabledMethod.Invoke(null, new object[] {k_LegacyEnabledSettingName, value}); + setCloudServiceEnabledMethod.Invoke(null, new object[] { k_LegacyEnabledSettingName, value }); } } } @@ -69,7 +66,7 @@ static bool GetLegacyEnabledSetting() var getCloudServiceEnabledMethod = playerSettingsType.GetMethod("GetCloudServiceEnabled", BindingFlags.Static | BindingFlags.NonPublic); if (getCloudServiceEnabledMethod != null) { - var enabledStateResult = getCloudServiceEnabledMethod.Invoke(null, new object[] {k_LegacyEnabledSettingName}); + var enabledStateResult = getCloudServiceEnabledMethod.Invoke(null, new object[] { k_LegacyEnabledSettingName }); isEnabled = Convert.ToBoolean(enabledStateResult); } } diff --git a/Editor/ServiceProjectSettings/PurchasingSettingsProvider.cs b/Editor/ServiceProjectSettings/PurchasingSettingsProvider.cs index d3cd1cf..9fc2163 100644 --- a/Editor/ServiceProjectSettings/PurchasingSettingsProvider.cs +++ b/Editor/ServiceProjectSettings/PurchasingSettingsProvider.cs @@ -12,8 +12,7 @@ internal class PurchasingSettingsProvider : EditorGameServiceSettingsProvider const string k_Title = "In-App Purchases"; const string k_Description = "Simplify cross platform In-App Purchasing"; - - PurchasingGameService m_Service; + readonly PurchasingGameService m_Service; bool m_CallbacksInitialized; SimpleStateMachine m_StateMachine; @@ -82,7 +81,7 @@ void RefreshDetailUI() var clonedContainer = SettingsUIUtils.CloneUIFromTemplate(UIResourceUtils.purchasingServicesRootUxmlPath); rootVisualElement.Add(clonedContainer); - var uiState = (BasePurchasingState) m_StateMachine.currentState; + var uiState = (BasePurchasingState)m_StateMachine.currentState; foreach (var uiStateElement in uiState.GetStateUI()) { diff --git a/Editor/ServiceProjectSettings/Service/GoogleConfigService.cs b/Editor/ServiceProjectSettings/Service/GoogleConfigService.cs index 3ee6c00..558ac12 100644 --- a/Editor/ServiceProjectSettings/Service/GoogleConfigService.cs +++ b/Editor/ServiceProjectSettings/Service/GoogleConfigService.cs @@ -2,15 +2,13 @@ namespace UnityEditor.Purchasing { internal class GoogleConfigService { - GoogleConfigurationData m_GoogleConfigData; - static GoogleConfigService m_Instance; - internal GoogleConfigurationData GoogleConfigData => m_GoogleConfigData; + internal GoogleConfigurationData GoogleConfigData { get; } GoogleConfigService() { - m_GoogleConfigData = new GoogleConfigurationData(); + GoogleConfigData = new GoogleConfigurationData(); } internal static GoogleConfigService Instance() diff --git a/Editor/ServiceProjectSettings/Service/Networking/GoogleConfigurationWebRequests.cs b/Editor/ServiceProjectSettings/Service/Networking/GoogleConfigurationWebRequests.cs index 1dae530..dd75c28 100644 --- a/Editor/ServiceProjectSettings/Service/Networking/GoogleConfigurationWebRequests.cs +++ b/Editor/ServiceProjectSettings/Service/Networking/GoogleConfigurationWebRequests.cs @@ -16,14 +16,12 @@ class GoogleConfigurationWebRequests const string k_AuthHeaderValueFormat = "Basic {0}"; const string k_ContentHeaderName = "Content-Type"; const string k_ContentHeaderValue = "application/json;charset=UTF-8"; - - IWebRequestInternal m_WebRequest = new CloudProjectWebRequest(); + readonly IWebRequestInternal m_WebRequest = new CloudProjectWebRequest(); UnityWebRequest m_GetGoogleKeyRequest; - GoogleConfigurationData m_PurchasingRemoteDataRef; - - Action m_GetGooglePlayKeyCallback; - Action m_SetGooglePlayKeyCallback; + readonly GoogleConfigurationData m_PurchasingRemoteDataRef; + readonly Action m_GetGooglePlayKeyCallback; + readonly Action m_SetGooglePlayKeyCallback; internal GoogleConfigurationWebRequests(GoogleConfigurationData remoteData, Action onGetGooglePlayKey, Action onSetGooglePlayKey) { @@ -90,7 +88,7 @@ void OnGetGooglePlayKey(AsyncOperation getKeyOperation) void FetchGooglePlayKeyFromRequest() { - string googlePlayKey = ""; + var googlePlayKey = ""; if (IsGoogleKeyRequestResultSuccess()) { try @@ -166,21 +164,11 @@ void OnSubmitGooglePlayKey(AsyncOperation pushKeyOperation) void HandleCompletedSubmitResponse(UnityWebRequest completedRequest) { - GooglePlayRevenueTrackingKeyState keyState; - - if (completedRequest.IsResultTransferSuccess()) - { - keyState = GooglePlayRevenueTrackingKeyState.Verified; - } - else if (completedRequest.IsResultProtocolError()) - { - keyState = InterpretKeyStateFromProtocolError(completedRequest.responseCode); - } - else - { - keyState = GooglePlayRevenueTrackingKeyState.InvalidFormat; - } - + var keyState = completedRequest.IsResultTransferSuccess() + ? GooglePlayRevenueTrackingKeyState.Verified + : completedRequest.IsResultProtocolError() + ? InterpretKeyStateFromProtocolError(completedRequest.responseCode) + : GooglePlayRevenueTrackingKeyState.InvalidFormat; m_SetGooglePlayKeyCallback(keyState); } diff --git a/Editor/ServiceProjectSettings/Service/Networking/NetworkingUtils.cs b/Editor/ServiceProjectSettings/Service/Networking/NetworkingUtils.cs index 8d4023e..8c198c9 100644 --- a/Editor/ServiceProjectSettings/Service/Networking/NetworkingUtils.cs +++ b/Editor/ServiceProjectSettings/Service/Networking/NetworkingUtils.cs @@ -20,8 +20,7 @@ internal static string GetValueFromJsonDictionary(string rawJson, string key) { var container = (Dictionary)MiniJson.JsonDecode(rawJson); - object value; - container.TryGetValue(key, out value); + container.TryGetValue(key, out var value); return value as string; } } diff --git a/Editor/ServiceProjectSettings/SimpleStateMachine.cs b/Editor/ServiceProjectSettings/SimpleStateMachine.cs index 4273260..4eabc8b 100644 --- a/Editor/ServiceProjectSettings/SimpleStateMachine.cs +++ b/Editor/ServiceProjectSettings/SimpleStateMachine.cs @@ -12,8 +12,8 @@ namespace UnityEditor.Purchasing /// class SimpleStateMachine { - HashSet m_Events = new HashSet(); - Dictionary m_StateByName = new Dictionary(); + readonly HashSet m_Events = new HashSet(); + readonly Dictionary m_StateByName = new Dictionary(); bool m_Initialized; /// @@ -73,7 +73,7 @@ public void AddEvent(T simpleStateMachineEvent) public bool EventExists(T simpleStateMachineEvent) { - foreach (T knownEvent in m_Events) + foreach (var knownEvent in m_Events) { if (knownEvent.Equals(simpleStateMachineEvent)) { @@ -164,7 +164,7 @@ public void ProcessEvent(T simpleStateMachineEvent) /// internal class State { - List m_ActionForEvent = new List(); + readonly List m_ActionForEvent = new List(); /// /// Access to the state machine. Mostly to GetStateByName when transitioning from one state to another @@ -243,7 +243,7 @@ State DoNothing(T simpleStateMachineEvent) /// It allows to do a common operation on the current state without having all the other states repeat this /// code within their transition actions. /// - public virtual void EnterState() {} + public virtual void EnterState() { } class ActionForEvent { diff --git a/Editor/ServiceProjectSettings/UI/UXML/AnalyticsNotice.uxml b/Editor/ServiceProjectSettings/UI/UXML/AnalyticsNotice.uxml deleted file mode 100644 index 8ecc71e..0000000 --- a/Editor/ServiceProjectSettings/UI/UXML/AnalyticsNotice.uxml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/Editor/ServiceProjectSettings/UI/UXML/AnalyticsNotice.uxml.meta b/Editor/ServiceProjectSettings/UI/UXML/AnalyticsNotice.uxml.meta deleted file mode 100644 index 3407628..0000000 --- a/Editor/ServiceProjectSettings/UI/UXML/AnalyticsNotice.uxml.meta +++ /dev/null @@ -1,10 +0,0 @@ -fileFormatVersion: 2 -guid: d660ad34b13f1b24fb2ccb859c8c5396 -ScriptedImporter: - internalIDToNameTable: [] - externalObjects: {} - serializedVersion: 2 - userData: - assetBundleName: - assetBundleVariant: - script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0} diff --git a/Editor/ServiceProjectSettings/UI/UXML/AnalyticsWarning.uxml b/Editor/ServiceProjectSettings/UI/UXML/AnalyticsWarning.uxml new file mode 100644 index 0000000..3b4463f --- /dev/null +++ b/Editor/ServiceProjectSettings/UI/UXML/AnalyticsWarning.uxml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/Editor/ServiceProjectSettings/UI/UXML/AnalyticsWarning.uxml.meta b/Editor/ServiceProjectSettings/UI/UXML/AnalyticsWarning.uxml.meta new file mode 100644 index 0000000..e4c997d --- /dev/null +++ b/Editor/ServiceProjectSettings/UI/UXML/AnalyticsWarning.uxml.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e19dce4e8a4c4ef58fe19536b828cc63 +timeCreated: 1652387645 \ No newline at end of file diff --git a/Editor/ServiceProjectSettings/UI/Views/AnalyticsNoticeBlock.cs b/Editor/ServiceProjectSettings/UI/Views/AnalyticsNoticeBlock.cs deleted file mode 100644 index 12086ba..0000000 --- a/Editor/ServiceProjectSettings/UI/Views/AnalyticsNoticeBlock.cs +++ /dev/null @@ -1,67 +0,0 @@ -using UnityEngine.UIElements; - -namespace UnityEditor.Purchasing -{ - internal class AnalyticsNoticeBlock : IPurchasingSettingsUIBlock - { - const string k_EnabledNoticeSectionName = "EnabledNoticeSection"; - const string k_DisabledNoticeSectionName = "DisabledNoticeSection"; - - private string m_ActiveSectionName; - - VisualElement m_NoticeBlock; - - internal static AnalyticsNoticeBlock CreateEnabledAnalyticsBlock() - { - return new AnalyticsNoticeBlock(k_EnabledNoticeSectionName); - } - - internal static AnalyticsNoticeBlock CreateDisabledAnalyticsBlock() - { - return new AnalyticsNoticeBlock(k_DisabledNoticeSectionName); - } - - private AnalyticsNoticeBlock(string activeSection) - { - m_ActiveSectionName = activeSection; - } - - public VisualElement GetUIBlockElement() - { - return SetupConfigBlock(); - } - - VisualElement SetupConfigBlock() - { - m_NoticeBlock = SettingsUIUtils.CloneUIFromTemplate(UIResourceUtils.analyticsNoticeUxmlPath); - - SetupNoticeBlock(); - SetupStyleSheets(); - - return m_NoticeBlock; - } - - void SetupNoticeBlock() - { - ToggleStateSectionVisibility(k_EnabledNoticeSectionName); - ToggleStateSectionVisibility(k_DisabledNoticeSectionName); - } - - void ToggleStateSectionVisibility(string sectionName) - { - var errorSection = m_NoticeBlock.Q(sectionName); - if (errorSection != null) - { - errorSection.style.display = (sectionName == m_ActiveSectionName) - ? DisplayStyle.Flex - : DisplayStyle.None; - } - } - - void SetupStyleSheets() - { - m_NoticeBlock.AddStyleSheetPath(UIResourceUtils.purchasingCommonUssPath); - m_NoticeBlock.AddStyleSheetPath(EditorGUIUtility.isProSkin ? UIResourceUtils.purchasingDarkUssPath : UIResourceUtils.purchasingLightUssPath); - } - } -} diff --git a/Editor/ServiceProjectSettings/UI/Views/AnalyticsWarningServiveSettingsBlock.cs b/Editor/ServiceProjectSettings/UI/Views/AnalyticsWarningServiveSettingsBlock.cs new file mode 100644 index 0000000..572694e --- /dev/null +++ b/Editor/ServiceProjectSettings/UI/Views/AnalyticsWarningServiveSettingsBlock.cs @@ -0,0 +1,22 @@ +using UnityEngine.UIElements; + +namespace UnityEditor.Purchasing +{ + internal class AnalyticsWarningSettingsBlock : IPurchasingSettingsUIBlock + { + VisualElement m_CatalogBlock; + + public VisualElement GetUIBlockElement() + { + m_CatalogBlock = SettingsUIUtils.CloneUIFromTemplate(UIResourceUtils.analyticsWarningUxmlPath); + SetupStyleSheets(); + return m_CatalogBlock; + } + + void SetupStyleSheets() + { + m_CatalogBlock.AddStyleSheetPath(UIResourceUtils.purchasingCommonUssPath); + m_CatalogBlock.AddStyleSheetPath(EditorGUIUtility.isProSkin ? UIResourceUtils.purchasingDarkUssPath : UIResourceUtils.purchasingLightUssPath); + } + } +} diff --git a/Editor/ServiceProjectSettings/UI/Views/AnalyticsWarningServiveSettingsBlock.cs.meta b/Editor/ServiceProjectSettings/UI/Views/AnalyticsWarningServiveSettingsBlock.cs.meta new file mode 100644 index 0000000..618f687 --- /dev/null +++ b/Editor/ServiceProjectSettings/UI/Views/AnalyticsWarningServiveSettingsBlock.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 472975f3563e410b9e580688252e8394 +timeCreated: 1652388481 \ No newline at end of file diff --git a/Editor/ServiceProjectSettings/UI/Views/AppleConfigurationSettingsBlock.cs b/Editor/ServiceProjectSettings/UI/Views/AppleConfigurationSettingsBlock.cs index 41ee288..e4c3f89 100644 --- a/Editor/ServiceProjectSettings/UI/Views/AppleConfigurationSettingsBlock.cs +++ b/Editor/ServiceProjectSettings/UI/Views/AppleConfigurationSettingsBlock.cs @@ -4,13 +4,11 @@ namespace UnityEditor.Purchasing { class AppleConfigurationSettingsBlock : IPurchasingSettingsUIBlock { - VisualElement m_AppleConfigurationBlock; - - AppleObfuscatorSection m_ObfuscatorSection; + readonly VisualElement m_AppleConfigurationBlock; + readonly AppleObfuscatorSection m_ObfuscatorSection; VisualElement m_ConfigurationBlock; - - string m_AppleErrorMessage; - string m_GoogleErrorMessage; + readonly string m_AppleErrorMessage; + readonly string m_GoogleErrorMessage; internal AppleConfigurationSettingsBlock() { diff --git a/Editor/ServiceProjectSettings/UI/Views/GooglePlayConfigurationSettingsBlock.cs b/Editor/ServiceProjectSettings/UI/Views/GooglePlayConfigurationSettingsBlock.cs index 65711ca..1071ef5 100644 --- a/Editor/ServiceProjectSettings/UI/Views/GooglePlayConfigurationSettingsBlock.cs +++ b/Editor/ServiceProjectSettings/UI/Views/GooglePlayConfigurationSettingsBlock.cs @@ -19,18 +19,16 @@ internal class GooglePlayConfigurationSettingsBlock : IPurchasingSettingsUIBlock const string k_GooglePlayKeyBtnUpdateLabel = "Update"; const string k_GooglePlayKeyBtnVerifyLabel = "Verify"; - - GoogleConfigurationData m_GooglePlayDataRef; - GoogleConfigurationWebRequests m_WebRequests; + readonly GoogleConfigurationData m_GooglePlayDataRef; + readonly GoogleConfigurationWebRequests m_WebRequests; VisualElement m_ConfigurationBlock; - - GoogleObfuscatorSection m_ObfuscatorSection; + readonly GoogleObfuscatorSection m_ObfuscatorSection; internal GooglePlayConfigurationSettingsBlock() { m_GooglePlayDataRef = GoogleConfigService.Instance().GoogleConfigData; - m_WebRequests = new GoogleConfigurationWebRequests(m_GooglePlayDataRef, this.OnGetGooglePlayKey, this.OnUpdateGooglePlayKey); + m_WebRequests = new GoogleConfigurationWebRequests(m_GooglePlayDataRef, OnGetGooglePlayKey, OnUpdateGooglePlayKey); m_ObfuscatorSection = new GoogleObfuscatorSection(m_GooglePlayDataRef); } @@ -87,7 +85,8 @@ void SetupButtonActions() googlePlayExternalLink.AddManipulator(clickable); } - m_ConfigurationBlock.Q(k_GooglePlayKeyEntry).RegisterValueChangedCallback(evt => { + m_ConfigurationBlock.Q(k_GooglePlayKeyEntry).RegisterValueChangedCallback(evt => + { m_GooglePlayDataRef.googlePlayKey = evt.newValue; }); } @@ -114,14 +113,9 @@ void ToggleUpdateButtonDisplay() var updateGooglePlayKeyBtn = m_ConfigurationBlock.Q