Cloud Codeを使用して購入を完了
Cloud Codeモジュールをバックエンド ソリューションとして実装して、購入に対応します。
読み終わるまでの所要時間 5 分最終更新 9日前
D2C支払いプロバイダーを使用する場合は、Cloud Codeモジュールを使用して購入を完了できます。 Cloud Code は Webhook 購入イベントを検証し、成功の反応を返します。以下の実装するにはモジュールを記述する必要があります。
- SKU(Stock Keeping単位)に基づいて、Cloud Saveなどのデータベースで製品資格を付与し、プレイヤーのインベントリを更新します。
- orders API を呼び出して、注文を履行済みとしてマークします。
orders APIの詳細については、イベント タイプのドキュメントを参照してください。
モジュールのデプロイ
モジュールのデプロイ方法については、『Cloud Code Get started guide』を参照してください。IAP ダッシュボードで Cloud Code モジュールを設定する
Unity DashboardでCloud Codeモジュールから購入情報を受け取るようにプロジェクトを設定します。- Unity Dashboard で、IAP> Payment Providers を選択します。
- Entitlement 配信メソッドで、編集 を選択します。
- Cloud Code モジュールオプションを選択し、ドロップダウンからモジュールとエンドポイントを選択します。
- Save configuration (設定の保存) を選択します。
ログの使用
Cloud Code モジュールがトランザクションを処理する場合、Cloud Code ダッシュボードでログをビューできます。詳細については、Cloud Code Logging のドキュメントを参照してください。Cloud Code サンプルモジュール
購入イベントの Webhook を処理する以下のモジュール例を参照してください。using System;using System.Collections.Generic;using System.Net.Http;using System.Net.Http.Headers;using System.Text;using System.Text.Json;using System.Text.Json.Serialization;using System.Threading;using System.Threading.Tasks;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Logging;using Unity.Services.CloudCode.Core;using Unity.Services.CloudCode.Apis;using Unity.Services.CloudSave.Model;名前空間の履行;// Handles IAP purchase webhooks: grants product entitlements via Cloud Save and marks orders as fulfilled.public class IapFulfillmentHandler{ private readonly ILogger<IapFulfillmentHandler> m_Logger; . private static readonly HttpClient s_HttpClient = new() { Timeout = TimeSpan.FromSeconds(10) }; private const string k_IapTransactionVerifierBaseUrl = "https://iap.services.api.unity.com/v1"; // Accepts and stores the logger used for diagnostics. public IapFulfillmentHandler(ILogger<IapFulfillmentHandler> logger) { m_Logger = logger; } // Webhook entry point: grants entitlements from line items via Cloud Save, then marks the order as fulfilled and returns a response. [CloudCodeFunction("ProcessPurchaseFulfillment")] public async Task<WebhookResponse> ProcessPurchaseFulfillment( IExecutionContext コンテキスト IGameApiClient gameApiClient, GUID ID 文字列バージョン、 文字列 eventType、 DateTime time、 string projectId, string environmentId, 文字列データ型、 WebhookOrderData data) { try { m_Logger.LogInformation( "購入処理 - EventID: {EventID}, EventType: {EventType}, Time: {Time}, " + "ProjectID: {ProjectID}, EnvironmentId: {EnvironmentId}, PlayerId: {PlayerId}, " + " "PaymentProvider: {PaymentProvider}, PaymentProviderResourceId: {PaymentProviderResourceId}, " + "OrderId: {OrderId}, LineItems: {LineItems}, CustomReferenceId: {CustomReferenceId}", id.ToString(), eventType time.ToString(), projectId environmentId, data.PlayerId, data.PaymentProvider, data.PaymentProviderResourceId, データ.Id、 JsonSerializer.Serialize(data.LineItems), data.CustomReferenceId); List<string> entitlements = await GrantProductEntitlements( context, gameApiClient, data.PlayerId, data.LineItems); m_Logger.LogInformation( " " +" の付いたオーダー {OrderId} の購入処理が正常に終了しました" "{Entitlements}", データ.Id、エンタイトルメント); try { m_Logger.LogInformation("Calling fulfillment API for order {OrderId}", data.Id); bool fulfillmentSuccess = await FulfillOrderAsync( データ.Id、 projectId environmentId, context.ServiceToken ?? string.Empty); if (fulfillmentSuccess) { m_Logger.LogInformation("Successfully marked order {OrderId} as fulfilled", data.Id); } else { m_Logger.LogWarning( "Failed to mark order {OrderId} as fulfilled, but entitlements was " (注文 {OrderId} を完了済みとしてマークできませんでしたが、エンタイトルメントは " + "granted", データ.Id); } } catch (例外) { m_Logger.LogError(ex, "Error calling fulfillment API for order {OrderId}: {Error}", データ.Id, ex.Message); } return new WebhookResponse { Status = WebhookResponseCodes.WebhookStatusOK }; } catch (例外) { m_Logger.LogError( "注文{OrderId}の履行中に予期しないエラーが発生しました: {Error}", データ.Id, ex.Message); return new WebhookResponse { Status = WebhookResponseCodes.WebhookStatusError, Code = WebhookResponseCodes.WebhookErrorDeclined, Description = ex.Message }; } } // Loads the player inventory, applies each line item by SKU (gems, pass, storage), saves inventory, and returns the list of granted entitlement descriptions. private async Task<List<string>> GrantProductEntitlements( IExecutionContext コンテキスト IGameApiClient gameApiClient, 文字列 playerId、 リスト<WebhookLineItem>ラインアイテム) { if (lineItems == null || lineItems.Count == 0) { m_Logger.LogWarning("GrantProductEntitlements called with null or empty line items list"); return new List<string>(); } リスト<string>エンタイトルメント = new リスト<string>(); try { m_Logger.LogInformation("Getting player inventory for player {PlayerId}", playerId); PlayerInventory currentInventory = await GetPlayerInventory( context, gameApiClient, playerId); m_Logger.LogInformation("Retrieved inventory: {Inventory}", JsonSerializer.Serialize(currentInventory)); Foreach (lineItems の WebhookLineItem 項目) { string sku = item.Sku; m_Logger.LogInformation("Processing line item:SKU={SKU}, ProductType={ProductType}", sku, 項目.ProductType); switch (sku) { case "com.unity.iap.test.adventure.pass.not": int days = 30; currentInventory.AdventurePass = DateTime.UtcNow.AddDays(days); entitlements.Add($"{days} Days Adventure Pass"); break; case "com.unity.iap.test.30.gems": int gems30 = 30; currentInventory.Gems += gems30; entitlements.Add($"{gems30} Gems"); break; case "com.unity.iap.test.premium.storage": currentInventory.PremiumStorage = true; entitlements.Add($"PremiumStorage = true"); break; デフォルト m_Logger.LogWarning("Unknown SKU {SKU} in fulfillment request", sku); break; } } m_Logger.LogInformation("Updated inventory: inventory: {Inventory}", JsonSerializer.Serialize(currentInventory)); m_Logger.LogInformation("Saving player inventory"); await SavePlayerInventory(context, gameApiClient, playerId, currentInventory); m_Logger.LogInformation("Successfully saved player inventory"); } catch (例外) { m_Logger.LogError("Error in GrantProductEntitlements: {Error}", ex.Message); throw; } return entitlements; } // Fetches the "player_inventory" key from Cloud Save for the player and deserializes it; returns a new empty inventory if missing or on error. private async Task<PlayerInventory> GetPlayerInventory( ) IExecutionContext コンテキスト IGameApiClient gameApiClient, string playerId) { try { var result = await gameApiClient.CloudSaveData.GetItemsAsync( context context.ServiceToken!, context.ProjectId!, PlayerId new List<string> { "player_inventory" }); if (result.Data.Results.Count <= 0) { m_Logger.LogInformation("No existing inventory found, creating new inventory"); return new PlayerInventory(); } var resultItem = result.Data.Results[0]; if (resultItem.Value == null) { m_Logger.LogWarning("Inventory value is null, creating new inventory"); return new PlayerInventory(); } string inventoryJson; try { inventoryJson = resultItem.Value.ToString(); if (string.IsNullOrWhiteSpace(inventoryJson)) { m_Logger.LogWarning("Inventory JSON is empty, creating new inventory"); return new PlayerInventory(); } } catch (例外) { m_Logger.LogError("Failed to convert inventory value to string: {Error}", ex.Message); return new PlayerInventory(); } try { PlayerInventory inventory = JsonSerializer.Deserialize<PlayerInventory>( inventoryJson); return inventory ?? new PlayerInventory(); } catch (例外) { m_Logger.LogError( インベントリのデシリアライズライズに失敗しました JSON: {Error}。JSON: {Json}", ex.Message, inventoryJson); return new PlayerInventory(); } } catch (例外) { m_Logger.LogError("Unexpected error while getting inventory: {Error}", ex.Message); return new PlayerInventory(); } } // Serializes the inventory to JSON and writes it to Cloud Save under the "player_inventory" key for the player. private async Task SavePlayerInventory( IExecutionContext コンテキスト IGameApiClient gameApiClient, 文字列 playerId、 PlayerInventory インベントリ) { try { if (inventory == null) { m_Logger.LogWarning("Attempted to save null inventory, skipping"); return; } string inventoryJson; try { inventoryJson = JsonSerializer.Serialize(inventory); } catch (例外) { m_Logger.LogError("Failed to serialize inventory: {Error}", ex.Message); throw; } await gameApiClient.CloudSaveData.SetItemAsync( context context.ServiceToken!, context.ProjectId!, PlayerId new SetItemBody("player_inventory", inventoryJson)); m_Logger.LogInformation("Successfully saved inventory for player {PlayerId}", playerId); } catch (例外) { m_Logger.LogError("Failed to save inventory: {Error}", ex.Message); throw; } } // Sends a PATCH request to the IAP orders API to set the order status to fulfilled; returns true on success. private async Task<bool> FulfillOrderAsync() 文字列 orderId、 string projectId, string environmentId, 文字列 serviceToken) { if (string.IsNullOrEmpty(orderId)) { m_Logger.LogWarning("Cannot fulfill order - order ID is null or empty"); return false; } if (string.IsNullOrEmpty(projectId)) { m_Logger.LogWarning("Cannot fulfill order - project ID is null or empty"); return false; } if (string.IsNullOrEmpty(environmentId)) { m_Logger.LogWarning("Cannot fulfill order - environment ID is null or empty"); return false; } if (string.IsNullOrEmpty(serviceToken)) { m_Logger.LogWarning("Cannot fulfill order - service token is null or empty"); return false; } try { string url = $"{k_IapTransactionVerifierBaseUrl}/projects/{projectId}/environments/" + $"{environmentId}/orders/{orderId}"; var requestBody = new { status = "fulfilled" }; string jsonContent = JsonSerializer.Serialize(requestBody); StringContent content = new StringContent( jsonContent, Encoding.UTF8, "application/json"); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Patch, url) { Content = content }; request.Headers.Authorization = new AuthenticationHeaderValue( "Bearer", serviceToken); m_Logger.LogInformation("Sending fulfillment request to {Url} for order {OrderId}", URL, orderId); HttpResponseMessage response = await s_HttpClient.SendAsync(request); if (response.IsSuccessStatusCode) { m_Logger.LogInformation("注文 {OrderId} の完了 API 呼び出しが成功しました。状態: {StatusCode}", orderId, response.StatusCode); return true; } string responseBody = await response.Content.ReadAsStringAsync(); m_Logger.LogWarning( "オーダー {OrderId} の完了 API 呼び出しが失敗しました。"状態: {StatusCode}, " + "反応: {ResponseBody}", orderId, response.StatusCode, responseBody); return false; } catch (例外) { m_Logger.LogError(ex, "Unexpected error while fulfilling order {OrderId}: {Error}", orderId, ex.Message); return false; } }}// Player inventory stored in Cloud Save (gems, adventure pass, premium storage, and similar).public class PlayerInventory{ public int Gems { get; set; } public int XpBooster { get; set; } public DateTime?AdventurePass { get; set; } public bool PremiumStorage { get; set; }}// Order payload sent by the IAP webhook for a purchase event.public class WebhookOrderData{ [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; [JsonPropertyName("playerId")] public string PlayerId { get; set; } = string.Empty; [JsonPropertyName("paymentProvider")] public string PaymentProvider { get; set; } = string.Empty; [JsonPropertyName("paymentProviderResourceId")] public string?PaymentProviderResourceId { get; set; } [JsonPropertyName("url")] public string Url { get; set; } = string.Empty; [JsonPropertyName("lineItems")] public List<WebhookLineItem> LineItems { get; set; } = new(); [JsonPropertyName("currency")] public string Currency { get; set; } = string.Empty; [JsonPropertyName("amounts")] public WebhookAmounts?Amounts { get; set; } [JsonPropertyName("status")] public string Status { get; set; } = string.Empty; [JsonPropertyName("customReferenceId")] public string?CustomReferenceId { get; set; } [JsonPropertyName("metadata")] public Dictionary<string, string>?Metadata { get; set; } [JsonPropertyName("createdAt")] public DateTime CreatedAt { get; set; } [JsonPropertyName("updatedAt")] public DateTime UpdatedAt { get; set; } [JsonPropertyName("paidAt")] public DateTime?PaidAt { get; set; } [JsonPropertyName("fulfilledAt")] public DateTime?FulfilledAt { get; set; }}// A single purchasable item in an order (SKU, product type, and price).public class WebhookLineItem{ [JsonPropertyName("sku")] public string Sku { get; set; } = string.Empty; [JsonPropertyName("productType")] public string ProductType { get; set; } = string.Empty; [JsonPropertyName("price")] public WebhookMoney Price { get; set; } = null!;}// Total and refunded amounts for an order, in micros.public class WebhookAmounts{ [JsonPropertyName("totalMicros")] public long TotalMicros { get; set; } [JsonPropertyName("refundedMicros")] public long RefundedMicros { get; set; }}// A monetary amount and currency (amount in micros).public class WebhookMoney{ [JsonPropertyName("amountMicros")] public long AmountMicros { get; set; } [JsonPropertyName("currency")] public string Currency { get; set; } = string.Empty;}// Status and error code constants for IAP webhook responses.public static class WebhookResponseCodes{ public const string WebhookStatusOK = "ok"; public const string WebhookStatusError = "error"; public const string WebhookErrorDeclined = "declined";}// Response returned by the IAP fulfillment webhook to the IAP service.public class WebhookResponse{ public string Status { get; set; } = string.Empty; public string?コード { get; set; } public string?Description { get; set; }}// Registers Cloud Code module dependencies (for example, the game API client).public class ModuleConfig :ICloudCodeSetup{ // Registers the game API client as a singleton for re-use by Cloud Code functions. public void Setup(ICloudCodeConfig config) { config.Dependencies.AddSingleton(GameApiClient.Create()); }}