파이어베이스로 인앱결제 및 영수증 검증(펑션,앱체크 활용) 2부에서 이어지는 글입니다.
코드가 있어서 그런가 글을 한번에 올리지 못한 점 대단히 죄송합니다..!
[시리즈] 럭키피스! 우당탕탕 개발일지
· (+홍보) 파이어베이스로 인앱결제 및 영수증 검증(펑션,앱체크 활용)_1 · 파이어베이스로 인앱결제 및 영수증 검증(펑션,앱체크 활용)_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;
}
}
혹시 파이어스토어같은 db 서비스도 쓰시나요? 이게 관리를 잘못하면 엄청 요금 폭탄 터질거같은데 어떻게 관리하시는지 팁이나 그런거 좀 얻을 수 있을까요?
db는 파이어스토어를 쓰고있습니다. 제 게임에선 유료 재화를 사거나 소비할 때, 혹은 일정 시간마다 직접 저장이 가능하게하도록 세팅해놓았습니다. 아직 실 서비스는 하지 않았기에 실제 요금은 얼마가 나올진 저도 모르겠지만, 재화 구매나 광고 시청당 서버 저장을 한다고 설계하면 단위당 청구되는 요금보단 광고나 인앱 수익이 더 클 것이기 때문에 서버에 저장하는 분기점을 최대한 줄이는게 가장 중요한 점이 아닐까 생각드네요.!