using System; using System.Collections.Generic; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using UnityEngine; using UnityEngine.Networking; using UnityEngine.Purchasing; namespace UnityEditor.Purchasing { /// /// Synchronize store data from UDP and IAP /// public static class UdpSynchronizationApi { internal const string kOAuthClientId = "channel_editor"; // Although a client secret is here, it doesn't matter // because the user information is also secured by user's token private const string kOAuthClientSecret = "B63AFB324DE3D12A13827340019D1EE3"; private const string kHttpVerbGET = "GET"; private const string kHttpVerbPOST = "POST"; private const string kHttpVerbPUT = "PUT"; private const string kContentType = "Content-Type"; private const string kApplicationJson = "application/json"; private const string kAuthHeader = "Authorization"; private static void CheckUdpBuildConfig() { var udpBuildConfig = BuildConfigInterface.GetClassType(); if (udpBuildConfig == null) { Debug.LogError("Cannot Retrieve Build Config Endpoints for UDP. Please make sure the UDP package is installed"); throw new NotImplementedException(); } } /// /// Get Access Token according to authCode. /// /// Acquired by UnityOAuth /// [Obsolete("Internal API, it will be removed soon.")] public static object GetAccessToken(string authCode) { return CreateGetAccessTokenRequest(authCode); } /// /// Create a Web Request to get the UDP Access Token according to authCode. /// /// Acquired by UnityOAuth /// internal static UnityWebRequest CreateGetAccessTokenRequest(string authCode) { CheckUdpBuildConfig(); var req = new TokenRequest { code = authCode, client_id = kOAuthClientId, client_secret = kOAuthClientSecret, grant_type = "authorization_code", redirect_uri = BuildConfigInterface.GetIdEndpoint() }; return AsyncRequest(kHttpVerbPOST, BuildConfigInterface.GetApiEndpoint(), "/v1/oauth2/token", null, req); } /// /// Call UDP store asynchronously to retrieve the Organization Identifier. /// /// The bearer token to UDP. /// The project id. /// The HTTP GET Request to get the organization identifier. [Obsolete("Internal API, it will be removed soon.")] public static object GetOrgId(string accessToken, string projectGuid) { return CreateGetOrgIdRequest(accessToken, projectGuid); } /// /// Call UDP store asynchronously to retrieve the Organization Identifier. /// /// The bearer token to UDP. /// The project id. /// The HTTP GET Request to get the organization identifier. internal static UnityWebRequest CreateGetOrgIdRequest(string accessToken, string projectGuid) { CheckUdpBuildConfig(); var api = "/v1/core/api/projects/" + projectGuid; return AsyncRequest(kHttpVerbGET, BuildConfigInterface.GetApiEndpoint(), api, accessToken, null); } /// /// Call UDP store asynchronously to create a store item. /// /// The bearer token to UDP. /// The organization identifier to create the store item under. /// The store item to create. /// The HTTP POST Request to create a store item. [Obsolete("Internal API, it will be removed soon.")] public static object CreateStoreItem(string accessToken, string orgId, IapItem iapItem) { return CreateAddStoreItemRequest(accessToken, orgId, iapItem); } /// /// Call UDP store asynchronously to create a store item. /// /// The bearer token to UDP. /// The organization identifier to create the store item under. /// The store item to create. /// The HTTP POST Request to create a store item. internal static UnityWebRequest CreateAddStoreItemRequest(string accessToken, string orgId, IapItem iapItem) { CheckUdpBuildConfig(); var api = "/v1/store/items"; iapItem.ownerId = orgId; return AsyncRequest(kHttpVerbPOST, BuildConfigInterface.GetUdpEndpoint(), api, accessToken, iapItem); } /// /// Call UDP store asynchronously to update a store item. /// /// The bearer token to UDP. /// The updated store item. /// The HTTP PUT Request to update a store item. [Obsolete("Internal API, it will be removed soon.")] public static object UpdateStoreItem(string accessToken, IapItem iapItem) { return CreateUpdateStoreItemRequest(accessToken, iapItem); } /// /// Call UDP store asynchronously to update a store item. /// /// The bearer token to UDP. /// The updated store item. /// The HTTP PUT Request to update a store item. internal static UnityWebRequest CreateUpdateStoreItemRequest(string accessToken, IapItem iapItem) { CheckUdpBuildConfig(); var api = "/v1/store/items/" + iapItem.id; return AsyncRequest(kHttpVerbPUT, BuildConfigInterface.GetUdpEndpoint(), api, accessToken, iapItem); } /// /// Call UDP store asynchronously to search for a store item. /// /// The bearer token to UDP. /// The organization identifier where to find the store item. /// The store item slug name. /// The HTTP GET Request to update a store item. [Obsolete("Internal API, it will be removed soon.")] public static object SearchStoreItem(string accessToken, string orgId, string appItemSlug) { return CreateSearchStoreItemRequest(accessToken, orgId, appItemSlug); } /// /// Call UDP store asynchronously to search for a store item. /// /// The bearer token to UDP. /// The organization identifier where to find the store item. /// The store item slug name. /// The HTTP GET Request to update a store item. internal static UnityWebRequest CreateSearchStoreItemRequest(string accessToken, string orgId, string appItemSlug) { CheckUdpBuildConfig(); var api = "/v1/store/items/search?ownerId=" + orgId + "&ownerType=ORGANIZATION&start=0&count=20&type=IAP&masterItemSlug=" + appItemSlug; return AsyncRequest(kHttpVerbGET, BuildConfigInterface.GetUdpEndpoint(), api, accessToken, null); } // Return UnityWebRequest instance static UnityWebRequest AsyncRequest(string method, string url, string api, string token, object postObject) { var request = new UnityWebRequest(url + api, method); if (postObject != null) { var postData = HandlePostData(JsonUtility.ToJson(postObject)); var postDataBytes = Encoding.UTF8.GetBytes(postData); request.uploadHandler = new UploadHandlerRaw(postDataBytes); } request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader(kContentType, kApplicationJson); if (token != null) { request.SetRequestHeader(kAuthHeader, "Bearer " + token); } request.SendWebRequest(); return request; } internal static bool CheckUdpAvailability() { return true; } internal static bool CheckUdpCompatibility() { var udpBuildConfig = BuildConfigInterface.GetClassType(); if (udpBuildConfig == null) { Debug.LogError("Cannot Retrieve Build Config Endpoints for UDP. Please make sure the UDP package is installed"); return false; } var udpVersion = BuildConfigInterface.GetVersion(); int.TryParse(udpVersion.Split('.')[0], out var majorVersion); return majorVersion >= 2; } // A very tricky way to deal with the json string, need to be improved // en-US and zh-CN will appear in the JSON and Unity JsonUtility cannot // recognize them to variables. So we change this to a string (remove "-"). private static string HandlePostData(string oldData) { var newData = oldData.Replace("thisShouldBeENHyphenUS", "en-US"); newData = newData.Replace("thisShouldBeZHHyphenCN", "zh-CN"); var re = new Regex("\"\\w+?\":\"\","); newData = re.Replace(newData, ""); re = new Regex(",\"\\w+?\":\"\""); newData = re.Replace(newData, ""); re = new Regex("\"\\w+?\":\"\""); newData = re.Replace(newData, ""); return newData; } } #region model /// /// This class is used to authenticate the API call to UDP. In OAuth2.0 authentication format. /// [Serializable] public class TokenRequest { /// /// The access token. Acquired by UnityOAuth /// public string code; /// /// The client identifier /// public string client_id; /// /// The client secret key /// public string client_secret; /// /// The type of OAuth2.0 code granting. /// public string grant_type; /// /// Redirect use after a successful authorization. /// public string redirect_uri; /// /// When the access token is expire. This token is used to renew it. /// public string refresh_token; } /// /// PriceSets holds the PurchaseFee. Used for IapItem. /// [Serializable] public class PriceSets { /// /// Get the PurchaseFee /// public PurchaseFee PurchaseFee = new PurchaseFee(); } /// /// A PurchaseFee contains the PriceMap which contains the prices and currencies. /// [Serializable] public class PurchaseFee { /// /// The PurchaseFee type /// public string priceType = "CUSTOM"; /// /// Holds a list of prices with their currencies /// public PriceMap priceMap = new PriceMap(); } /// /// PriceMap hold a list of PriceDetail. /// [Serializable] public class PriceMap { /// /// List of prices with their currencies. /// public List DEFAULT = new List(); } /// /// Price and the currency of a IAPItem. /// [Serializable] public class PriceDetail { /// /// Price of a IAPItem. /// public string price; /// /// Currency of the price. /// public string currency = "USD"; } /// /// The Response from and HTTP response converted into an object. /// [Serializable] public class GeneralResponse { /// /// The body from the HTTP response. /// public string message; } /// /// The properties of a IAPItem. /// [Serializable] public class Properties { /// /// The description of a IAPItem. /// public string description; } /// /// The response used when creating/updating IAP item succeeds /// [Serializable] public class IapItemResponse : GeneralResponse { /// /// The IapItem identifier. /// public string id; } /// /// IapItem is the representation of a purchasable product from the UDP store /// [Serializable] public class IapItem { /// /// A unique identifier to identify the product. /// public string id; /// /// The product url stripped of all unsafe characters. /// public string slug; /// /// The product name. /// public string name; /// /// The organization url stripped of all unsafe characters. /// public string masterItemSlug; /// /// Is product a consumable type. If set to false it is a subscriptions. /// Consumables may be purchased more than once. /// Subscriptions have a finite window of validity. /// public bool consumable = true; /// /// The product type. /// public string type = "IAP"; /// /// The product status. /// public string status = "STAGE"; /// /// The organization id. /// public string ownerId; /// /// The organization type. /// public string ownerType = "ORGANIZATION"; /// /// The product's prices with currencies. /// public PriceSets priceSets = new PriceSets(); /// /// The properties of the product. /// public Properties properties = new Properties(); /// /// Validates that the IapItem has at least the minimum amount of information set. /// /// A string error of missing information to the IapItem. public string ValidationCheck() { if (string.IsNullOrEmpty(slug)) { return "Please fill in the ID"; } if (string.IsNullOrEmpty(name)) { return "Please fill in the title"; } if (properties == null || string.IsNullOrEmpty(properties.description)) { return "Please fill in the description"; } return ""; } } /// /// TokenInfo holds all the authentication token required to authenticate the API call. /// [Serializable] public class TokenInfo : GeneralResponse { /// /// The OAuth2.0 access token. /// public string access_token; /// /// The OAuth2.0 refresh token. /// public string refresh_token; } /// /// The response used when searching for IAP item. /// [Serializable] public class IapItemSearchResponse : GeneralResponse { /// /// The total amount of IAP item found. /// public int total; /// /// The list of IAP item found. /// public List results; } struct ReqStruct { public UnityWebRequest request; public GeneralResponse resp; public ProductCatalogEditor.ProductCatalogItemEditor itemEditor; public IapItem iapItem; } /// /// The response used when searching for Organization identifier. /// [Serializable] public class OrgIdResponse : GeneralResponse { /// /// The organization identifier. /// public string org_foreign_key; } /// /// The response used when searching for Organization roles. /// [Serializable] public class OrgRoleResponse : GeneralResponse { /// /// The organization roles. /// public List roles; } /// /// The response used when getting an error. /// [Serializable] public class ErrorResponse : GeneralResponse { /// /// The http error code. /// public string code; /// /// The details of an error. /// public ErrorDetail[] details; } /// /// The details of an error return from the api. /// [Serializable] public class ErrorDetail { /// /// The error context where it occured. /// public string field; /// /// The error message reason. /// public string reason; } #endregion }