lib_unity_purchase/Editor/UdpSynchronizationApi.cs

550 lines
18 KiB
C#
Raw Normal View History

2024-01-29 18:49:33 +08:00
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
{
/// <summary>
/// Synchronize store data from UDP and IAP
/// </summary>
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();
}
}
/// <summary>
/// Get Access Token according to authCode.
/// </summary>
/// <param name="authCode"> Acquired by UnityOAuth</param>
/// <returns></returns>
[Obsolete("Internal API, it will be removed soon.")]
public static object GetAccessToken(string authCode)
{
return CreateGetAccessTokenRequest(authCode);
}
/// <summary>
/// Create a Web Request to get the UDP Access Token according to authCode.
/// </summary>
/// <param name="authCode">Acquired by UnityOAuth</param>
/// <returns></returns>
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);
}
/// <summary>
/// Call UDP store asynchronously to retrieve the Organization Identifier.
/// </summary>
/// <param name="accessToken">The bearer token to UDP.</param>
/// <param name="projectGuid">The project id.</param>
/// <returns>The HTTP GET Request to get the organization identifier.</returns>
[Obsolete("Internal API, it will be removed soon.")]
public static object GetOrgId(string accessToken, string projectGuid)
{
return CreateGetOrgIdRequest(accessToken, projectGuid);
}
/// <summary>
/// Call UDP store asynchronously to retrieve the Organization Identifier.
/// </summary>
/// <param name="accessToken">The bearer token to UDP.</param>
/// <param name="projectGuid">The project id.</param>
/// <returns>The HTTP GET Request to get the organization identifier.</returns>
internal static UnityWebRequest CreateGetOrgIdRequest(string accessToken, string projectGuid)
{
CheckUdpBuildConfig();
var api = "/v1/core/api/projects/" + projectGuid;
return AsyncRequest(kHttpVerbGET, BuildConfigInterface.GetApiEndpoint(), api, accessToken, null);
}
/// <summary>
/// Call UDP store asynchronously to create a store item.
/// </summary>
/// <param name="accessToken">The bearer token to UDP.</param>
/// <param name="orgId">The organization identifier to create the store item under.</param>
/// <param name="iapItem">The store item to create.</param>
/// <returns>The HTTP POST Request to create a store item.</returns>
[Obsolete("Internal API, it will be removed soon.")]
public static object CreateStoreItem(string accessToken, string orgId, IapItem iapItem)
{
return CreateAddStoreItemRequest(accessToken, orgId, iapItem);
}
/// <summary>
/// Call UDP store asynchronously to create a store item.
/// </summary>
/// <param name="accessToken">The bearer token to UDP.</param>
/// <param name="orgId">The organization identifier to create the store item under.</param>
/// <param name="iapItem">The store item to create.</param>
/// <returns>The HTTP POST Request to create a store item.</returns>
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);
}
/// <summary>
/// Call UDP store asynchronously to update a store item.
/// </summary>
/// <param name="accessToken">The bearer token to UDP.</param>
/// <param name="iapItem">The updated store item.</param>
/// <returns>The HTTP PUT Request to update a store item.</returns>
[Obsolete("Internal API, it will be removed soon.")]
public static object UpdateStoreItem(string accessToken, IapItem iapItem)
{
return CreateUpdateStoreItemRequest(accessToken, iapItem);
}
/// <summary>
/// Call UDP store asynchronously to update a store item.
/// </summary>
/// <param name="accessToken">The bearer token to UDP.</param>
/// <param name="iapItem">The updated store item.</param>
/// <returns>The HTTP PUT Request to update a store item.</returns>
internal static UnityWebRequest CreateUpdateStoreItemRequest(string accessToken, IapItem iapItem)
{
CheckUdpBuildConfig();
var api = "/v1/store/items/" + iapItem.id;
return AsyncRequest(kHttpVerbPUT, BuildConfigInterface.GetUdpEndpoint(), api, accessToken, iapItem);
}
/// <summary>
/// Call UDP store asynchronously to search for a store item.
/// </summary>
/// <param name="accessToken">The bearer token to UDP.</param>
/// <param name="orgId">The organization identifier where to find the store item.</param>
/// <param name="appItemSlug">The store item slug name.</param>
/// <returns>The HTTP GET Request to update a store item.</returns>
[Obsolete("Internal API, it will be removed soon.")]
public static object SearchStoreItem(string accessToken, string orgId, string appItemSlug)
{
return CreateSearchStoreItemRequest(accessToken, orgId, appItemSlug);
}
/// <summary>
/// Call UDP store asynchronously to search for a store item.
/// </summary>
/// <param name="accessToken">The bearer token to UDP.</param>
/// <param name="orgId">The organization identifier where to find the store item.</param>
/// <param name="appItemSlug">The store item slug name.</param>
/// <returns>The HTTP GET Request to update a store item.</returns>
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
/// <summary>
/// This class is used to authenticate the API call to UDP. In OAuth2.0 authentication format.
/// </summary>
[Serializable]
public class TokenRequest
{
/// <summary>
/// The access token. Acquired by UnityOAuth
/// </summary>
public string code;
/// <summary>
/// The client identifier
/// </summary>
public string client_id;
/// <summary>
/// The client secret key
/// </summary>
public string client_secret;
/// <summary>
/// The type of OAuth2.0 code granting.
/// </summary>
public string grant_type;
/// <summary>
/// Redirect use after a successful authorization.
/// </summary>
public string redirect_uri;
/// <summary>
/// When the access token is expire. This token is used to renew it.
/// </summary>
public string refresh_token;
}
/// <summary>
/// PriceSets holds the PurchaseFee. Used for IapItem.
/// </summary>
[Serializable]
public class PriceSets
{
/// <summary>
/// Get the PurchaseFee
/// </summary>
public PurchaseFee PurchaseFee = new PurchaseFee();
}
/// <summary>
/// A PurchaseFee contains the PriceMap which contains the prices and currencies.
/// </summary>
[Serializable]
public class PurchaseFee
{
/// <summary>
/// The PurchaseFee type
/// </summary>
public string priceType = "CUSTOM";
/// <summary>
/// Holds a list of prices with their currencies
/// </summary>
public PriceMap priceMap = new PriceMap();
}
/// <summary>
/// PriceMap hold a list of PriceDetail.
/// </summary>
[Serializable]
public class PriceMap
{
/// <summary>
/// List of prices with their currencies.
/// </summary>
public List<PriceDetail> DEFAULT = new List<PriceDetail>();
}
/// <summary>
/// Price and the currency of a IAPItem.
/// </summary>
[Serializable]
public class PriceDetail
{
/// <summary>
/// Price of a IAPItem.
/// </summary>
public string price;
/// <summary>
/// Currency of the price.
/// </summary>
public string currency = "USD";
}
/// <summary>
/// The Response from and HTTP response converted into an object.
/// </summary>
[Serializable]
public class GeneralResponse
{
/// <summary>
/// The body from the HTTP response.
/// </summary>
public string message;
}
/// <summary>
/// The properties of a IAPItem.
/// </summary>
[Serializable]
public class Properties
{
/// <summary>
/// The description of a IAPItem.
/// </summary>
public string description;
}
/// <summary>
/// The response used when creating/updating IAP item succeeds
/// </summary>
[Serializable]
public class IapItemResponse : GeneralResponse
{
/// <summary>
/// The IapItem identifier.
/// </summary>
public string id;
}
/// <summary>
/// IapItem is the representation of a purchasable product from the UDP store
/// </summary>
[Serializable]
public class IapItem
{
/// <summary>
/// A unique identifier to identify the product.
/// </summary>
public string id;
/// <summary>
/// The product url stripped of all unsafe characters.
/// </summary>
public string slug;
/// <summary>
/// The product name.
/// </summary>
public string name;
/// <summary>
/// The organization url stripped of all unsafe characters.
/// </summary>
public string masterItemSlug;
/// <summary>
/// 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.
/// </summary>
public bool consumable = true;
/// <summary>
/// The product type.
/// </summary>
public string type = "IAP";
/// <summary>
/// The product status.
/// </summary>
public string status = "STAGE";
/// <summary>
/// The organization id.
/// </summary>
public string ownerId;
/// <summary>
/// The organization type.
/// </summary>
public string ownerType = "ORGANIZATION";
/// <summary>
/// The product's prices with currencies.
/// </summary>
public PriceSets priceSets = new PriceSets();
/// <summary>
/// The properties of the product.
/// </summary>
public Properties properties = new Properties();
/// <summary>
/// Validates that the IapItem has at least the minimum amount of information set.
/// </summary>
/// <returns>A string error of missing information to the IapItem.</returns>
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 "";
}
}
/// <summary>
/// TokenInfo holds all the authentication token required to authenticate the API call.
/// </summary>
[Serializable]
public class TokenInfo : GeneralResponse
{
/// <summary>
/// The OAuth2.0 access token.
/// </summary>
public string access_token;
/// <summary>
/// The OAuth2.0 refresh token.
/// </summary>
public string refresh_token;
}
/// <summary>
/// The response used when searching for IAP item.
/// </summary>
[Serializable]
public class IapItemSearchResponse : GeneralResponse
{
/// <summary>
/// The total amount of IAP item found.
/// </summary>
public int total;
/// <summary>
/// The list of IAP item found.
/// </summary>
public List<IapItem> results;
}
struct ReqStruct
{
public UnityWebRequest request;
public GeneralResponse resp;
public ProductCatalogEditor.ProductCatalogItemEditor itemEditor;
public IapItem iapItem;
}
/// <summary>
/// The response used when searching for Organization identifier.
/// </summary>
[Serializable]
public class OrgIdResponse : GeneralResponse
{
/// <summary>
/// The organization identifier.
/// </summary>
public string org_foreign_key;
}
/// <summary>
/// The response used when searching for Organization roles.
/// </summary>
[Serializable]
public class OrgRoleResponse : GeneralResponse
{
/// <summary>
/// The organization roles.
/// </summary>
public List<string> roles;
}
/// <summary>
/// The response used when getting an error.
/// </summary>
[Serializable]
public class ErrorResponse : GeneralResponse
{
/// <summary>
/// The http error code.
/// </summary>
public string code;
/// <summary>
/// The details of an error.
/// </summary>
public ErrorDetail[] details;
}
/// <summary>
/// The details of an error return from the api.
/// </summary>
[Serializable]
public class ErrorDetail
{
/// <summary>
/// The error context where it occured.
/// </summary>
public string field;
/// <summary>
/// The error message reason.
/// </summary>
public string reason;
}
#endregion
}