La API de Billing te permite vender suscripciones y artículos digitales. El wrapper de C# proporciona una interfaz asíncrona y con seguridad de tipos para la Biblioteca de Facturación Google Play subyacente.
Espacio de nombres: PlayPcSdkManaged.Billing
Clase de cliente: BillingClient
Crea el cliente
Siempre usa la fábrica para crear un BillingClient. Esto garantiza que las devoluciones de llamada seguras para Unity se registren automáticamente.
using UnityEngine; using System; using System.Threading.Tasks; using System.Collections.Generic; // Required SDK Namespaces using PlayPcSdkManaged.Billing; using PlayPcSdkManaged.Unity; public class BillingManager : MonoBehaviour { private BillingClient _billingClient; public void SetupBilling() { try { // Creates the client with the required UnityBillingCallbacksHandler _billingClient = PlayPcSdkFactory.CreateBillingClient(); Debug.Log("Billing Client created successfully."); } catch (Exception ex) { Debug.LogError($"Failed to create Billing Client: {ex.Message}"); } } private void OnDestroy() { // Always dispose of the client to clean up native C++ resources _billingClient?.Dispose(); } }
Cómo consultar los detalles del producto
Antes de mostrar los elementos a la venta, debes consultar detalles como el precio, el título y la descripción en Google Play.
public async Task GetProductDetailsAsync() { try { // Define the list of products you want to query var productList = new List{ // Use ProductType.InApp for consumables/non-consumables new ProductId { Id = "gem_pack_100", ProductType = ProductType.InApp }, // Use ProductType.Subs for subscriptions new ProductId { Id = "gold_subscription", ProductType = ProductType.InApp } }; var queryParams = new QueryProductDetailsParams { ProductIds = productList }; // Async call var result = await _billingClient.QueryProductDetailsAsync(queryParams); if (result.IsOk) { foreach (var product in result.Value.ProductDetailsList) { // The formatted price (e.g., "$0.99") is inside the ProductOffers list if (product.ProductOffers != null && product.ProductOffers.Count > 0) { var price = product.ProductOffers[0].FormattedPrice; var offerToken = product.ProductOffers[0].OfferToken; Debug.Log($"Product: {product.Title} | Price: {price} | Token: {offerToken}"); } } } else { Debug.LogError($"Query Failed: {result.Code} - {result.ErrorMessage}"); } } catch (Exception ex) { Debug.LogException(ex); } }
Inicia el flujo de compra
Para iniciar una compra, llama a LaunchPurchaseFlowAsync con el OfferToken del paso de detalles del producto.
public async Task BuyItemAsync(string offerToken) { try { var purchaseParams = new LaunchPurchaseFlowParams { OfferToken = offerToken, Quantity = 1, // Optional: Attach obfuscated IDs for fraud detection ObfuscatedAccountId = "user_12345_hash", ObfuscatedProfileId = "profile_abcde_hash" }; var result = await _billingClient.LaunchPurchaseFlowAsync(purchaseParams); if (result.IsOk) { var purchase = result.Value.ProductPurchaseDetails; Debug.Log($"Purchase Successful! Order ID: {purchase.OrderId}"); // IMPORTANT: You must now Acknowledge or Consume this purchase. // If you don't, Google Play will refund the user after a few days. if (!purchase.IsAcknowledged) { // Decide based on your game logic if it's consumable or permanent await HandlePurchaseAsync(purchase); } } else if (result.Code == BillingError.UserCanceled) { Debug.Log("User canceled the purchase flow."); } else { Debug.LogError($"Purchase Failed: {result.Code} - {result.ErrorMessage}"); } } catch (Exception ex) { Debug.LogException(ex); } }
Consulta las compras existentes (restablecimiento)
Llama a QueryPurchasesAsync al iniciar el juego para restablecer los elementos que ya tienes, por ejemplo, después de reinstalar el juego, o para verificar si hay transacciones pendientes.
En este ejemplo, se muestra cómo enrutar las compras a Acknowledge para elementos permanentes o a Consume para elementos consumibles.
public async Task CheckExistingPurchasesAsync() { try { // Fetches all purchases owned by the user var result = await _billingClient.QueryPurchasesAsync(); if (result.IsOk) { foreach (var purchase in result.Value.ProductPurchaseDetails) { Debug.Log($"User owns: {purchase.ProductId} | State: {purchase.PurchaseState}"); // Process any purchase that hasn't been acknowledged yet if (purchase.PurchaseState == PurchaseState.Purchased && !purchase.IsAcknowledged) { await HandlePurchaseAsync(purchase); } } } else { Debug.LogError($"Restore Failed: {result.Code} - {result.ErrorMessage}"); } } catch (Exception ex) { Debug.LogException(ex); } } // Helper method to route purchases private async Task HandlePurchaseAsync(ProductPurchaseDetails purchase) { // Example logic: "gem_pack" is consumable, everything else is permanent if (purchase.ProductId.Contains("gem_pack")) { await ConsumeItemAsync(purchase.PurchaseToken); } else { await AcknowledgeItemAsync(purchase.PurchaseToken); } }
Cómo confirmar una compra
Se deben confirmar los artículos no consumibles (por ejemplo, "Actualización premium" o "Paquete de niveles"). Esto indica a Google Play que le otorgaste el elemento al usuario.
public async Task AcknowledgeItemAsync(string purchaseToken) { try { var acknowledgeParams = new AcknowledgePurchaseParams { PurchaseToken = purchaseToken }; var result = await _billingClient.AcknowledgePurchaseAsync(acknowledgeParams); if (result.IsOk) { Debug.Log("Purchase Acknowledged. Usage rights granted permanently."); } else { Debug.LogError($"Acknowledge Failed: {result.Code} - {result.ErrorMessage}"); } } catch (Exception ex) { Debug.LogException(ex); } }
Cómo consumir una compra
Los elementos consumibles, como "100 gemas" o "Poción de salud", se deben consumir para permitir la recompra.
public async Task ConsumeItemAsync(string purchaseToken) { try { var consumeParams = new ConsumePurchaseParams { PurchaseToken = purchaseToken }; var result = await _billingClient.ConsumePurchaseAsync(consumeParams); if (result.IsOk) { Debug.Log("Item Consumed. User can buy it again."); // Add the gems/coins to the user's inventory here } else { Debug.LogError($"Consume Failed: {result.Code} - {result.ErrorMessage}"); } } catch (Exception ex) { Debug.LogException(ex); } }