0ab2df21e9d707817cbec6bd1fd20d3019269901cc809a53e6edbdd020891c06f0c1ff


파이어베이스로 인앱결제 및 영수증 검증(펑션,앱체크 활용) 2부에서 이어지는 글입니다.


코드가 있어서 그런가 글을 한번에 올리지 못한 점 대단히 죄송합니다..!






[ 3. 인앱결제 코드 ]


[선행 조건]

인앱 상품이 등록되어 있어야함. 인앱 시스템이 유니티에 구축되어있어야함.


1,2부에서의 설정을 완료했다면 이제는 이 모든 걸 인앱을 구현하는 코드로 구현하면 되겠지.


대략적으로 구매 버튼을 누르면 functions에 요청을 보내고 app-check가 된 영수증을 받아와 정상적으로 판명되면 다시 서버로 데이터를 저장하는 식이지..!


아래에는 해당 구조로 진행되는 인앱 코드고 

인앱 상품의 개인적인 데이터 + 상품 데이터를 서버에 저장 + 에러 처리 방법 빼고는 그대로 사용하면 되지 않을까 싶어.


서버에 저장하는 파이어스토어에 데이터를 저장하는 방법은 시간이되면 올려볼게.


나처럼 혼자서 끙끙 문제 해결을 하고있는 개발자분들에게 도움이 되길 바라며! 긴 정보글 읽어줘서 고마워!!


using System.Collections;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using DarkTonic.MasterAudio;
using UnityEngine;
using UnityEngine.Purchasing;
using Firebase.Functions;
using System;

public class IAPManager : MonoBehaviour, IStoreListener
{
private static IAPManager instance = null;
public static IAPManager Instance
{
get
{
if (instance == null)
{
var obj = FindObjectOfType<IAPManager>();
if (obj != null)
{
instance = obj;
}
else
{
var newObj = new GameObject().AddComponent<IAPManager>();
instance = newObj;
}
}
return instance;
}
}

public void Awake()
{
// 싱글톤
var objs = FindObjectsOfType<IAPManager>();
if(objs.Length != 1)
{
Destroy(gameObject);
return;
}
DontDestroyOnLoad(gameObject);
}

private IStoreController storeController;

// * 인앱 상품 별도 작성 필요
private string sculpture_2 = "sculpture_2";
private string sculpture_3 = "sculpture_3";

public void InitIAP()
{
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());

builder.AddProduct(sculpture_2, ProductType.Consumable);
builder.AddProduct(sculpture_3, ProductType.Consumable);

UnityPurchasing.Initialize(this, builder);
}

public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
storeController = controller;
Debug.Log("IAP 초기화 성공");
}

public void OnInitializeFailed(InitializationFailureReason error)
{
Debug.Log("IAP 초기화 실패 " + error);
}

public void OnInitializeFailed(InitializationFailureReason error, string message)
{
Debug.Log("IAP 초기화 실패 " + error + message);
}

public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
Debug.Log("구매 실패: " + failureReason);
}

public void Click_Purchase(string id)
{
storeController.InitiatePurchase(id);
}

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
{
var product = purchaseEvent.purchasedProduct;

Debug.Log("구매 시도: " + product.definition.id);

// 영수증 검증 후 완료 처리
ValidateReceiptWithFirebase(product).Forget();

// 구매 보류 처리 → 검증 성공 시 ConfirmPendingPurchase() 호출
return PurchaseProcessingResult.Pending;
}

private async UniTaskVoid ValidateReceiptWithFirebase(Product product)
{
// 결제 진행 시 로딩 패널 활성화
MainUi.instance.panel_server_connecting.SetActive(true);

string productId = product.definition.id;
string purchaseToken = product.receipt;

#if UNITY_ANDROID
var receiptWrapper = JsonUtility.FromJson<GooglePurchaseReceiptWrapper>(product.receipt);
if (receiptWrapper != null && !string.IsNullOrEmpty(receiptWrapper.Payload))
{
var payload = JsonUtility.FromJson<GooglePayload>(receiptWrapper.Payload);
if (payload != null && !string.IsNullOrEmpty(payload.json))
{
var purchaseData = JsonUtility.FromJson<GooglePurchaseData>(payload.json);
if (purchaseData != null && !string.IsNullOrEmpty(purchaseData.purchaseToken))
{
purchaseToken = purchaseData.purchaseToken;
}
else
{
Debug.LogError("purchaseToken이 비어있습니다. purchaseData 확인 필요.");
MainUi.instance.panel_server_connecting.SetActive(false);
return;
}
}
else
{
Debug.LogError("Payload.json 파싱 실패");
MainUi.instance.panel_server_connecting.SetActive(false);
return;
}
}
else
{
Debug.LogError("receiptWrapper 또는 Payload가 null입니다.");
MainUi.instance.panel_server_connecting.SetActive(false);
return;
}
#endif

Debug.Log($"[Firebase] 검증 시작: productId={productId}, token={purchaseToken}");

var functions = FirebaseFunctions.DefaultInstance;
var data = new Dictionary<string, object>
{
{ "productId", productId },
{ "purchaseToken", purchaseToken }
};

try
{
var function = functions.GetHttpsCallable("validateReceipt");
var result = await function.CallAsync(data);

if (result.Data == null)
{
Debug.LogError("Firebase 응답이 null입니다.");
// *에러 발생시 별도 처리 필요
ErrorManager.Instance.Error_Server();
}
else
{
Debug.Log($"Firebase 응답 타입: {result.Data.GetType()}");

// 응답 객체 처리 방식 개선
bool isValid = false;

if (result.Data is IDictionary dictionary)
{
// 모든 딕셔너리 타입으로부터 안전하게 값을 추출
foreach (DictionaryEntry entry in dictionary)
{
string key = entry.Key.ToString();
if (key == "isValid" && entry.Value != null)
{
isValid = Convert.ToBoolean(entry.Value);
Debug.Log($"isValid 값 추출: {isValid}");
break;
}
}

if (isValid)
{
Debug.Log("Firebase 구매 검증 성공");

// 유료 상품 데이터 파이어베이스 저장 진행 * 별도 작성 필요
if (productId == sculpture_2)
await ProcessSculpturePurchase(GameManager.Type_Sculpture.Sculpture_2, 10);
else if (productId == sculpture_3)
await ProcessSculpturePurchase(GameManager.Type_Sculpture.Sculpture_3, 10);

// 구매 완료 처리
storeController.ConfirmPendingPurchase(product);
}
else
{
// *에러 발생시 별도 처리 필요
ErrorManager.Instance.Error_Server();
Debug.LogWarning("Firebase 구매 검증 실패: 영수증이 유효하지 않음");
}
}
else
{
// *에러 발생시 별도 처리 필요
ErrorManager.Instance.Error_Server();
Debug.LogError($"Firebase 응답 형식이 딕셔너리가 아닙니다: {result.Data.GetType()}");
}
}
}
catch (Firebase.FirebaseException e)
{
Debug.LogError($"Firebase 오류: {e.Message} (Code: {e.ErrorCode})");

if ((FunctionsErrorCode)e.ErrorCode == FunctionsErrorCode.FailedPrecondition)
Debug.LogError("App Check 실패 - 토큰 없음 또는 유효하지 않음");

ErrorManager.Instance.Error_Server();
}
catch (Exception e)
{
ErrorManager.Instance.Error_Server();
Debug.LogError("Firebase 검증 오류: " + e.Message);
}

MainUi.instance.panel_server_connecting.SetActive(false);
}

private async UniTask ProcessSculpturePurchase(GameManager.Type_Sculpture type, int amount)
{
// 파이어베이스에 해당 데이터를 저장 * 별도 작성 필요
await FirebaseDataSystem.Instance.SaveFirebase_Rewarded_Ad_Sculpture(type, amount);
}


// 안드로이드 영수증 구조용 클래스
[System.Serializable]
public class GooglePurchaseReceiptWrapper
{
public string Store;
public string TransactionID;
public string Payload; // Payload를 문자열로 정의
}

[System.Serializable]
public class GooglePayload
{
public string json;
public string signature;
}

[System.Serializable]
public class GooglePurchaseData
{
public string orderId;
public string packageName;
public string productId;
public long purchaseTime;
public int purchaseState;
public string purchaseToken;
}
}