ドキュメント

Cloud Codeを使用して購入を完了

Cloud Codeモジュールをバックエンド ソリューションとして実装して、購入に対応します。
読み終わるまでの所要時間 12 分最終更新 3ヶ月前

D2C支払いプロバイダーを使用する場合は、Cloud Codeモジュールを使用して購入を完了できます。
Cloud Code は Webhook 購入イベントを検証し、成功の反応を返します。以下の実装するにはモジュールを記述する必要があります。
  1. SKU(Stock Keeping単位)に基づいて、Cloud Saveなどのデータベースで製品資格を付与し、プレイヤーのインベントリを更新します。
  2. orders API を呼び出して、注文を履行済みとしてマークします。
重要
注文を約定済みとしてマークすることは必須であり、任意ではありません。プレイヤーはどちらの場合でもエンタイトルメントを維持しますが、
paid
状態のままのオーダーでは、Unity Iapオーダーのトラッキングと分析は不完全のままになります。
orders APIの詳細については、イベント タイプのドキュメントを参照してください。

モジュールのデプロイ

モジュールのデプロイ方法については、『Cloud Code Get started guide』を参照してください。

IAP ダッシュボードで Cloud Code モジュールを設定する

Unity DashboardでCloud Codeモジュールから購入情報を受け取るようにプロジェクトを設定します。
  1. Unity Dashboard で、IAP> Payment Providers を選択します。
  2. Entitlement 配信メソッドで、編集 を選択します。
  3. Cloud Code モジュールオプションを選択し、ドロップダウンからモジュールとエンドポイントを選択します。
  4. 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()); }}