처음에 Unity에 Codeless를 사용해서 IAP를 사용하고 있었는데
처음 Scene이 IAP Button들이 있는 곳이 아니다보니 Android에서 Codeless초기화 때 작동시켜주는 Restore를 얻지 못해서
결국 수동으로 IAP를 관리해주는 Manager를 만들었습니다.
Manager사용 예시는 다음 게시글에 올리겠습니다.
- IDetailedStoreListener를 상속받아서 IStoreController, IExtensionProvider를 받을 수 있게 해주었습니다.
- 구매관련은 IStoreController를 사용, Restore는 IExtensionProvider를 사용하였습니다.
- 구매 후에는 ProcessPurchase여기로 Callback이 들어오는데 이때 정확한 delegate를 작동하기 위해 purchaseEventsMap에 mapping해두었습니다.
- ReplayRestoredPurchases함수를 통해 비소모, 구독 상품들의 구매복원을 가능하도록 했습니다.
- RestorePurchases는 Codeless의 BaseIAPButton.cs를 참고하여 작성하였습니다. 임의의 조건으로 저렇게 작성한 것이 아니기에 수정 때는 조심하세요.
- 만약 Codeless관련 된 Component가 어딘가에 붙어 있다면 IAP Catalog에 Automatically initialize UnityPurchasing을 체크 해제해도 Codeless를 Initilize하려하니 조심하세요.
전체코드
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;
[DefaultExecutionOrder(-100)]
public class IAPManager : Singleton<IAPManager>, IDetailedStoreListener
{
/// <summary>
/// Occurs when the restoration of transactions is completed.
/// </summary>
public event Action<bool, string> OnTransactionsRestored;
/// <summary>
/// Maps product IDs to their respective purchase event callbacks.
/// </summary>
private Dictionary<string, PurchaseEvents> purchaseEventsMap
= new Dictionary<string, PurchaseEvents>(50);
private struct PurchaseEvents
{
// Invoked on successful purchase.
public Action<Product> OnPurchaseCompleteEvent;
// Invoked on purchase failure with details.
public Action<Product, PurchaseFailureDescription> OnPurchaseFailedEvent;
// Invoked when product data is retrieved.
public Action<Product> OnPurchaseFetchedEvent;
}
/// <summary>
/// Automatically confirms consumable items.
/// </summary>
[SerializeField] private bool consumeConfirm = false;
public IStoreController StoreController;
private IExtensionProvider extensionProvider;
protected override void Awake()
{
base.Awake();
transform.SetParent(null);
DontDestroyOnLoad(gameObject);
}
private void Start()
{
ConfigurationBuilder builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
ProductCatalog catalog = ProductCatalog.LoadDefaultCatalog();
foreach (ProductCatalogItem product in catalog.allProducts)
{
builder.AddProduct(product.id, product.type);
}
UnityPurchasing.Initialize(this, builder);
}
public void AddIAPListener
(string productId, Action<Product> purchaseCompleteEvent,
Action<Product, PurchaseFailureDescription> purchaseFailedEvent,
Action<Product> purchaseFetchedEvent)
{
purchaseEventsMap[productId] = new PurchaseEvents
{
OnPurchaseCompleteEvent = purchaseCompleteEvent,
OnPurchaseFailedEvent = purchaseFailedEvent,
};
Product product = StoreController.products.WithID(productId);
if (product is not null)
{
purchaseFetchedEvent.Invoke(product);
}
else
{
Debug.LogError($"Product {productId} not found in storeController.products.");
}
}
public void RemoveIAPListener(string productId)
{
if (purchaseEventsMap.ContainsKey(productId))
{
purchaseEventsMap.Remove(productId);
}
}
/// <summary>
/// Type of event fired after a successful fetching the product information from the store.
/// </summary>
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
Debug.Log($"IAP Initialized {(controller is not null ? "Complete" : "Failed")}");
StoreController = controller;
extensionProvider = extensions;
if(StoreController is null) { return; }
foreach (Product product in StoreController.products.all)
{
if(purchaseEventsMap.TryGetValue(product.definition.id, out PurchaseEvents purchaseEvents))
{
purchaseEvents.OnPurchaseFetchedEvent?.Invoke(product);
}
}
}
/// <summary>
/// Type of event fired after a failed fetching the product information from the store.
/// </summary>
public void OnInitializeFailed(InitializationFailureReason error)
{
Debug.LogError($"IAP Initialization Failed: {error}");
}
/// <summary>
/// Type of event fired after a failed fetching the product information from the store.
/// </summary>
public void OnInitializeFailed(InitializationFailureReason error, string message)
{
Debug.LogError($"IAP Initialization Failed: {error}: {message}");
}
/// <summary>
/// Type of event fired after a failed purchase of a product.
/// </summary>
public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
{
Debug.LogError($"IAP Purchase Failed {product}: {failureDescription}");
if(purchaseEventsMap.TryGetValue(product.definition.id, out PurchaseEvents purchaseEvents))
{
purchaseEvents.OnPurchaseFailedEvent?.Invoke(product, failureDescription);
}
}
/// <summary>
/// Type of event fired after a failed purchase of a product.
/// </summary>
public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
Debug.LogError($"Purchase failed for product {product.definition.id}: {failureReason}");
}
/// <summary>
/// Type of event fired after a successful purchase of a product.
/// </summary>
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchase)
{
Debug.Log($"Purchase successful: {purchase.purchasedProduct.definition.id}");
if(purchaseEventsMap.TryGetValue
(purchase.purchasedProduct.definition.id, out PurchaseEvents purchaseEvents))
{
purchaseEvents.OnPurchaseCompleteEvent?.Invoke(purchase.purchasedProduct);
}
return consumeConfirm ? PurchaseProcessingResult.Complete : PurchaseProcessingResult.Pending;
}
/// <summary>
/// Type of event fired after a restore transactions was completed.
/// </summary>
private void TransactionsRestoredHandler(bool success, string error)
{
OnTransactionsRestored?.Invoke(success, error);
}
/// <summary>
/// Replays restore completion only for registered products that currently have receipts.
/// </summary>
public void ReplayRestoredPurchases()
{
if (StoreController is null) { return; }
foreach (KeyValuePair<string, PurchaseEvents> entry in purchaseEventsMap)
{
Product product = StoreController.products.WithID(entry.Key);
if (product is null)
{
UnityEngine.Debug.LogWarning($"ReplayRestoredPurchases product not found: {entry.Key}");
continue;
}
if (!product.hasReceipt)
{
UnityEngine.Debug.Log($"ReplayRestoredPurchases no receipt: {entry.Key}");
continue;
}
if (product.definition.type == ProductType.Consumable)
{
UnityEngine.Debug.Log($"ReplayRestoredPurchases skip consumable: {entry.Key}");
continue;
}
UnityEngine.Debug.Log($"ReplayRestoredPurchases invoke: {entry.Key}");
entry.Value.OnPurchaseCompleteEvent?.Invoke(product);
}
}
/// <summary>
/// For advanced scripted store-specific IAP actions, use this session's <typeparamref name="IStoreExtension"/>s after initialization.
/// </summary>
/// <typeparam name="T">A subclass of <typeparamref name="IStoreExtension"/> such as <typeparamref name="IAppleExtensions"/></typeparam>
/// <returns></returns>
public T GetStoreExtensions<T>() where T : IStoreExtension
{
return extensionProvider.GetExtension<T>();
}
/// <summary>
/// Starts the restore purchases process depending on the current platform.
/// Reference: BaseIAPButton.cs
/// </summary>
public void RestorePurchases()
{
// Check initialization
if (extensionProvider is null)
{
Debug.LogWarning("IAP is not initialized, cannot start restore process.");
return;
}
// 1. Windows Store (UWP)
if (Application.platform == RuntimePlatform.WSAPlayerX86 ||
Application.platform == RuntimePlatform.WSAPlayerX64 ||
Application.platform == RuntimePlatform.WSAPlayerARM)
{
Debug.Log("Starting Microsoft Store restore...");
extensionProvider.GetExtension<IMicrosoftExtensions>().RestoreTransactions();
}
// 2. Execute manual restore logic only for iOS or macOS
else if (Application.platform == RuntimePlatform.IPhonePlayer ||
Application.platform == RuntimePlatform.OSXPlayer ||
Application.platform == RuntimePlatform.tvOS
#if UNITY_VISIONOS
|| Application.platform == RuntimePlatform.VisionOS
#endif
)
{
Debug.Log("Starting Apple restore...");
extensionProvider.GetExtension<IAppleExtensions>().RestoreTransactions(TransactionsRestoredHandler);
}
// 3. Google Play (Android specific versions or explicit restore required)
else if (Application.platform == RuntimePlatform.Android &&
StandardPurchasingModule.Instance().appStore == AppStore.GooglePlay)
{
Debug.Log("Starting Google Play restore...");
extensionProvider.GetExtension<IGooglePlayStoreExtensions>().RestoreTransactions(TransactionsRestoredHandler);
}
else
{
Debug.LogWarning($"{Application.platform} does not support manual restore button.");
}
}
}'Unity' 카테고리의 다른 글
| Unity AR World와 3D World 같이 쓸 때 주의사항 (0) | 2026.04.25 |
|---|---|
| OperationException : Failed to initialize localization, could not preload asset tables (0) | 2026.03.13 |
| Unity Stove PCSDK3 연동과 Rank연동 후기 (0) | 2026.03.08 |
| Unity Codeless IAP 사용 때 소비아이템 자동 구매 확정 문제 해결 (0) | 2026.03.05 |
| Unity TextMeshPro - Text에 Sprite Mask 되게 적용하기 (0) | 2026.02.06 |